From 1c00e235657c81ad106dc7e07e7cea30b6d74243 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Thu, 31 Oct 2019 16:50:53 +0100 Subject: [PATCH] fix(tooltip): render tooltip on portal to avoid hidden overflows (#418) This commit render the tooltips (crosshair and annotation) into a react-portal to avoid begin hidden by the container overflow settings. It will also enhance the current tooltip positioning reducing the jump from one position to another only if the tooltip is touching the edges of the chart. close #375 --- .playground/index.html | 20 +- .playground/playgroud.tsx | 245 ++++++- integration/page_objects/common.ts | 73 +- ...rotation-visually-looks-correct-1-snap.png | Bin 0 -> 12449 bytes ...tooltip-on-first-x-value-bottom-1-snap.png | Bin 0 -> 18822 bytes ...ws-tooltip-on-first-x-value-top-1-snap.png | Bin 0 -> 20232 bytes ...-tooltip-on-last-x-value-bottom-1-snap.png | Bin 0 -> 19048 bytes ...ows-tooltip-on-last-x-value-top-1-snap.png | Bin 0 -> 20586 bytes ...tooltip-on-first-x-value-bottom-1-snap.png | Bin 0 -> 17820 bytes ...ws-tooltip-on-first-x-value-top-1-snap.png | Bin 0 -> 17603 bytes ...-tooltip-on-last-x-value-bottom-1-snap.png | Bin 0 -> 16019 bytes ...ows-tooltip-on-last-x-value-top-1-snap.png | Bin 0 -> 15730 bytes integration/tests/interactions.test.ts | 80 +++ .../annotations/annotation_marker.test.tsx | 260 ++----- .../annotations/annotation_tooltip.ts | 35 + .../annotations/annotation_utils.test.ts | 570 ++++++--------- .../xy_chart/annotations/annotation_utils.ts | 674 +++++++----------- .../crosshair/crosshair_utils.test.ts | 180 +++++ .../xy_chart/crosshair/crosshair_utils.ts | 147 ++-- .../xy_chart/specs/line_annotation.tsx | 1 + .../store/chart_state.interactions.test.ts | 8 +- .../xy_chart/store/chart_state.test.ts | 7 +- src/chart_types/xy_chart/store/chart_state.ts | 47 +- src/chart_types/xy_chart/utils/specs.ts | 4 +- src/components/_annotation.scss | 4 + src/components/_tooltip.scss | 11 +- src/components/annotation_tooltips.tsx | 121 +++- src/components/chart.tsx | 12 +- .../react_canvas/line_annotation.tsx | 8 +- .../react_canvas/reactive_chart.tsx | 1 - .../react_canvas/rect_annotation.tsx | 1 - src/components/tooltips.tsx | 112 ++- src/utils/data_generators/data_generator.ts | 8 +- stories/bar_chart.tsx | 24 + 34 files changed, 1492 insertions(+), 1161 deletions(-) create mode 100644 integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-tooltip-and-rotation-visually-looks-correct-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-first-x-value-bottom-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-first-x-value-top-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-last-x-value-bottom-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-last-x-value-top-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-first-x-value-bottom-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-first-x-value-top-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-last-x-value-bottom-1-snap.png create mode 100644 integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-last-x-value-top-1-snap.png create mode 100644 integration/tests/interactions.test.ts create mode 100644 src/chart_types/xy_chart/annotations/annotation_tooltip.ts create mode 100644 src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts diff --git a/.playground/index.html b/.playground/index.html index 6f31981cf3..f8f02b0f63 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -12,19 +12,29 @@ background: blanchedalmond !important; margin: 0; padding: 0; + height: 2000px; + width: 2000; } #root { - position: absolute; - top: 10px; - left: 10px; + top: 700px; + left: 800px; } .chart { background: white; + display: inline-block; position: relative; - width: 600px; - height: 350px; + width: 900px; + height: 600px; margin: 10px; + /* overflow: hidden; */ + } + button { + padding: 10px !important; + background: black !important; + border: 1px solid white !important; + color: white !important; + margin: 4px !important; } diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index 374ccba481..6d0ba39a52 100644 --- a/.playground/playgroud.tsx +++ b/.playground/playgroud.tsx @@ -1,48 +1,227 @@ import React, { Fragment } from 'react'; -import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, BarSeries, Settings, niceTimeFormatter } from '../src'; +import { + Axis, + Chart, + getAxisId, + getSpecId, + Position, + ScaleType, + Settings, + LineAnnotation, + getAnnotationId, + AnnotationDomainTypes, + RectAnnotation, + Rotation, + HistogramBarSeries, +} from '../src'; import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; - -export class Playground extends React.Component<{}, { dataLimit: boolean }> { +import { LoremIpsum } from 'lorem-ipsum'; +const lorem = new LoremIpsum({ + sentencesPerParagraph: { + max: 8, + min: 4, + }, + wordsPerSentence: { + max: 16, + min: 4, + }, +}); +interface State { + showChart1: boolean; + rotation: number; + vAxis: number; + hAxis: number; +} +const ROTATIONS: Rotation[] = [0, 90, -90, 180]; +const RECT_ANNOTATION_TEXT = lorem.generateSentences(3); +export class Playground extends React.Component<{}, State> { state = { - dataLimit: false, + showChart1: true, + rotation: 0, + vAxis: 0, + hAxis: 0, + }; + rotateChart = (evt: React.MouseEvent) => { + const sense = evt.metaKey ? -1 : 1; + + this.setState((prevState) => { + const val = prevState.rotation + sense < 0 ? 4 + sense : prevState.rotation + sense; + return { + rotation: val % 4, + }; + }); + }; + rotateVerticalAxis = () => { + this.setState((prevState) => { + return { + vAxis: (prevState.vAxis + 1) % 2, + }; + }); + }; + rotateHorizontalAxis = () => { + this.setState((prevState) => { + return { + hAxis: (prevState.hAxis + 1) % 2, + }; + }); }; - changeData = () => { + removeChart = () => { this.setState((prevState) => { return { - dataLimit: !prevState.dataLimit, + showChart1: !prevState.showChart1, }; }); }; render() { - const { data } = KIBANA_METRICS.metrics.kibana_os_load[0]; + const data = KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 30); return ( - -
- -
-
- - - - + this.state.showChart1 && ( + +
+ + + + +
+
+ + + + + + + + + + + + + - - -
-
+ + {/* + */} + + X ANN
} + /> + Y ANN} + /> + + +
+ ) ); } } diff --git a/integration/page_objects/common.ts b/integration/page_objects/common.ts index b049b2ac65..2178a2eba8 100644 --- a/integration/page_objects/common.ts +++ b/integration/page_objects/common.ts @@ -21,25 +21,29 @@ class CommonPage { return `${baseUrl}?${query}${query ? '&' : ''}knob-debug=false`; } - - async screenshotDOMElement(selector: string, opts?: ScreenshotDOMElementOptions) { - const padding: number = opts && opts.padding ? opts.padding : 0; - const path: string | undefined = opts && opts.path ? opts.path : undefined; - - await page.waitForSelector(selector, { timeout: 10000 }); - const rect = await page.evaluate((selector) => { + async getBoundingClientRect(selector = '.echChart[data-ech-render-complete=true]') { + return await page.evaluate((selector) => { const element = document.querySelector(selector); if (!element) { - return null; + throw Error(`Could not find element that matches selector: ${selector}.`); } const { x, y, width, height } = element.getBoundingClientRect(); return { left: x, top: y, width, height, id: element.id }; }, selector); - - if (!rect) throw Error(`Could not find element that matches selector: ${selector}.`); + } + /** + * Capture screenshot or chart element only + */ + async screenshotDOMElement( + selector = '.echChart[data-ech-render-complete=true]', + opts?: ScreenshotDOMElementOptions, + ) { + const padding: number = opts && opts.padding ? opts.padding : 0; + const path: string | undefined = opts && opts.path ? opts.path : undefined; + const rect = await this.getBoundingClientRect(selector); return page.screenshot({ path, @@ -52,11 +56,12 @@ class CommonPage { }); } - /** - * Capture screenshot or chart element only - */ - async getChartScreenshot() { - return this.screenshotDOMElement('.echChart[data-ech-render-complete=true]'); + async moveMouseRelativeToDOMElement( + mousePosition: { x: number; y: number }, + selector = '.echChart[data-ech-render-complete=true]', + ) { + const chartContainer = await this.getBoundingClientRect(selector); + await page.mouse.move(chartContainer.left + mousePosition.x, chartContainer.top + mousePosition.y); } /** @@ -68,10 +73,34 @@ class CommonPage { */ async expectChartAtUrlToMatchScreenshot(url: string) { try { - const cleanUrl = CommonPage.parseUrl(url); - await page.goto(cleanUrl); - const chart = await this.getChartScreenshot(); + await this.loadChartFromURL(url); + await this.waitForElement(); + + const chart = await this.screenshotDOMElement(); + + if (!chart) { + throw new Error(`Error: Unable to find chart element\n\n\t${url}`); + } + expect(chart).toMatchImageSnapshot(); + } catch (error) { + throw new Error(error); + } + } + + /** + * Expect a chart given a url from storybook. + * + * - Note: No need to fix host or port. They will be set automatically. + * + * @param url Storybook url from knobs section + */ + async expectChartWithMouseAtUrlToMatchScreenshot(url: string, mousePosition: { x: number; y: number }) { + try { + await this.loadChartFromURL(url); + await this.waitForElement(); + await this.moveMouseRelativeToDOMElement(mousePosition); + const chart = await this.screenshotDOMElement(); if (!chart) { throw new Error(`Error: Unable to find chart element\n\n\t${url}`); } @@ -81,6 +110,14 @@ class CommonPage { throw new Error(error); } } + async loadChartFromURL(url: string) { + const cleanUrl = CommonPage.parseUrl(url); + await page.goto(cleanUrl); + } + + async waitForElement(selector = '.echChart[data-ech-render-complete=true]', timeout = 10000) { + await page.waitForSelector(selector, { timeout }); + } } export const common = new CommonPage(); diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-tooltip-and-rotation-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-bar-chart-test-tooltip-and-rotation-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..602a8b754089a49d2473847d8c414b01b2230134 GIT binary patch literal 12449 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 xp%*R_myq7D`GXD;AuVm$x= literal 0 HcmV?d00001 diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-first-x-value-top-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-first-x-value-top-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..88d02087a248085c691178e2d2008186837c86ef GIT binary patch literal 20232 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 Pkxs~|$R-}syZ*lbJdw71 literal 0 HcmV?d00001 diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-last-x-value-bottom-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-0-shows-tooltip-on-last-x-value-bottom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..16861c7d185317d1dd34c147bff338f8ff8ca31c GIT binary patch literal 19048 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 Q9!Qcqd0r;*#I?Ks51(~=)&Kwi literal 0 HcmV?d00001 diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-first-x-value-bottom-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-first-x-value-bottom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..4f0d5892eb157c979e190d8871c52f86f77c99ea GIT binary patch literal 17820 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 literal 0 HcmV?d00001 diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-last-x-value-bottom-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-last-x-value-bottom-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..85fd73e86dec9bd1e3f0dae627a61b2e2d99e464 GIT binary patch literal 16019 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 F{~!1>0(1ZX literal 0 HcmV?d00001 diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-last-x-value-top-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-last-x-value-top-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..132adfcc4e84df1459b672f560d8505febeab78b GIT binary patch literal 15730 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 Zl|_rgLn^1?Ng*aCjI!3n{0mp_{tq=RbZ-Cv literal 0 HcmV?d00001 diff --git a/integration/tests/interactions.test.ts b/integration/tests/interactions.test.ts new file mode 100644 index 0000000000..69736b4fc8 --- /dev/null +++ b/integration/tests/interactions.test.ts @@ -0,0 +1,80 @@ +import { common } from '../page_objects'; + +describe.only('Tooltips', () => { + describe('rotation 0', () => { + it('shows tooltip on first x value - top', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation', + { + x: 160, + y: 25, + }, + ); + }); + it('shows tooltip on last x value - top', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation', + { + x: 660, + y: 25, + }, + ); + }); + it('shows tooltip on first x value - bottom', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation', + { + x: 160, + y: 280, + }, + ); + }); + it('shows tooltip on last x value - bottom', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation', + { + x: 660, + y: 280, + }, + ); + }); + }); + describe('rotation 90', () => { + it('shows tooltip on first x value - top', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation&knob-chartRotation=90', + { + x: 125, + y: 50, + }, + ); + }); + it('shows tooltip on last x value - top', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation&knob-chartRotation=90', + { + x: 700, + y: 50, + }, + ); + }); + it('shows tooltip on first x value - bottom', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation&knob-chartRotation=90', + { + x: 125, + y: 270, + }, + ); + }); + it('shows tooltip on last x value - bottom', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/bar-chart--test-tooltip-and-rotation&knob-chartRotation=90', + { + x: 700, + y: 270, + }, + ); + }); + }); +}); diff --git a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx index befdd685b1..dfa7e1c5de 100644 --- a/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx +++ b/src/chart_types/xy_chart/annotations/annotation_marker.test.tsx @@ -1,18 +1,17 @@ import * as React from 'react'; -import { AnnotationDomainType, AnnotationDomainTypes, AnnotationSpec, Position, Rotation } from '../utils/specs'; +import { AnnotationDomainTypes, AnnotationSpec, Position, Rotation } from '../utils/specs'; import { DEFAULT_ANNOTATION_LINE_STYLE } from '../../../utils/themes/theme'; import { Dimensions } from '../../../utils/dimensions'; import { getAnnotationId, getGroupId, GroupId } from '../../../utils/ids'; import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; import { Scale, ScaleType } from '../../../utils/scales/scales'; import { - AnnotationLinePosition, computeLineAnnotationDimensions, - DEFAULT_LINE_OVERFLOW, - isWithinLineBounds, + isWithinLineMarkerBounds, + AnnotationMarker, + AnnotationLineProps, } from './annotation_utils'; -import { Point } from '../store/chart_state'; describe('annotation marker', () => { const groupId = getGroupId('foo-group'); @@ -63,16 +62,30 @@ describe('annotation marker', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [DEFAULT_LINE_OVERFLOW, 20, 10, 20], + anchor: { + position: Position.Left, + top: 20, + left: 0, + }, + linePathPoints: { + start: { + x1: 0, + y1: 20, + }, + end: { + x2: 10, + y2: 20, + }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, 20, 10, 20], + marker: { icon:
, - transform: 'translate(calc(0px - 0%),calc(20px - 50%))', color: '#777', - dimensions: { width: 0, height: 0 }, + dimension: { width: 0, height: 0 }, + position: { left: -0, top: 20 }, }, }, ]; @@ -103,16 +116,29 @@ describe('annotation marker', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [DEFAULT_LINE_OVERFLOW, 20, 10, 20], + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, + linePathPoints: { + start: { + x1: 0, + y1: 20, + }, + end: { + x2: 10, + y2: 20, + }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, 20, 10, 20], marker: { icon:
, - transform: 'translate(calc(0px - 0%),calc(0px - 50%))', color: '#777', - dimensions: { width: 0, height: 0 }, + dimension: { width: 0, height: 0 }, + position: { left: -0, top: 0 }, }, }, ]; @@ -139,202 +165,58 @@ describe('annotation marker', () => { chartRotation, yScales, xScale, - Position.Left, + Position.Bottom, 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [20, -DEFAULT_LINE_OVERFLOW, 20, 20], + anchor: { + position: Position.Bottom, + top: 20, + left: 20, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [20, 0, 20, 20], + linePathPoints: { + start: { + x1: 20, + y1: 20, + }, + end: { + x2: 20, + y2: 0, + }, + }, marker: { icon:
, - transform: 'translate(calc(20px - 0%),calc(20px - 50%))', color: '#777', - dimensions: { width: 0, height: 0 }, + dimension: { width: 0, height: 0 }, + position: { top: 20, left: 20 }, }, }, ]; expect(dimensions).toEqual(expectedDimensions); }); - test('should compute if a point is within an annotation line bounds (xDomain annotation)', () => { - const linePosition1: AnnotationLinePosition = [10, 0, 10, 20]; - const cursorPosition1: Point = { x: 0, y: 0 }; - const cursorPosition2: Point = { x: 10, y: 0 }; - - const offset = 0; - const horizontalChartRotation: Rotation = 0; - const verticalChartRotation: Rotation = 90; - const domainType: AnnotationDomainType = AnnotationDomainTypes.XDomain; - - const marker = { + test('should compute if a point is within an annotation line marker bounds', () => { + const marker: AnnotationMarker = { icon:
, - transform: '', color: 'custom-color', - dimensions: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + dimension: { width: 10, height: 10 }, }; + expect(isWithinLineMarkerBounds({ x: -1, y: 0 }, marker)).toBe(false); - const bottomHorizontalRotationOutsideBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - cursorPosition1, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - marker, - ); + expect(isWithinLineMarkerBounds({ x: 0, y: -1 }, marker)).toBe(false); - expect(bottomHorizontalRotationOutsideBounds).toBe(false); + expect(isWithinLineMarkerBounds({ x: 0, y: 0 }, marker)).toBe(true); - const bottomHorizontalRotationWithinBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - cursorPosition2, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(bottomHorizontalRotationWithinBounds).toBe(true); - - const topHorizontalRotationOutsideBounds = isWithinLineBounds( - Position.Top, - linePosition1, - cursorPosition1, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(topHorizontalRotationOutsideBounds).toBe(false); - - const verticalRotationOutsideBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - cursorPosition1, - offset, - verticalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(verticalRotationOutsideBounds).toBe(false); - - const verticalRotationMarkerOutsideBounds = isWithinLineBounds( - Position.Bottom, - [0, 0, 0, 0], - { x: 0, y: 10 }, - offset, - verticalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(verticalRotationMarkerOutsideBounds).toBe(false); - }); + expect(isWithinLineMarkerBounds({ x: 10, y: 10 }, marker)).toBe(true); - test('should compute if a point is within an annotation line bounds (yDomain annotation)', () => { - const linePosition1: AnnotationLinePosition = [10, 0, 10, 20]; - const cursorPosition1: Point = { x: 0, y: 0 }; - const cursorPosition2: Point = { x: 10, y: 0 }; + expect(isWithinLineMarkerBounds({ x: 11, y: 10 }, marker)).toBe(false); - const offset = 0; - const horizontalChartRotation: Rotation = 0; - const verticalChartRotation: Rotation = 90; - const domainType: AnnotationDomainType = AnnotationDomainTypes.YDomain; - - const marker = { - icon:
, - transform: '', - color: 'custom-color', - dimensions: { width: 10, height: 10 }, - }; - - const rightHorizontalRotationWithinBounds = isWithinLineBounds( - Position.Left, - linePosition1, - cursorPosition1, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(rightHorizontalRotationWithinBounds).toBe(true); - - const leftHorizontalRotationWithinBounds = isWithinLineBounds( - Position.Left, - linePosition1, - cursorPosition2, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(leftHorizontalRotationWithinBounds).toBe(true); - - const rightHorizontalRotationOutsideBounds = isWithinLineBounds( - Position.Right, - linePosition1, - cursorPosition1, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(rightHorizontalRotationOutsideBounds).toBe(false); - - const verticalRotationOutsideBounds = isWithinLineBounds( - Position.Left, - linePosition1, - cursorPosition1, - offset, - verticalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(verticalRotationOutsideBounds).toBe(false); - - const verticalRotationMarkerOutsideBounds = isWithinLineBounds( - Position.Left, - [0, 0, 0, 0], - { x: 0, y: 10 }, - offset, - verticalChartRotation, - chartDimensions, - domainType, - marker, - ); - - expect(verticalRotationMarkerOutsideBounds).toBe(false); - - const verticalRotationMarkerWithinBounds = isWithinLineBounds( - Position.Left, - [10, 20, 10, 0], - { x: -5, y: 20 }, - offset, - verticalChartRotation, - chartDimensions, - domainType, - marker, - ); + expect(isWithinLineMarkerBounds({ x: 11, y: 10 }, { ...marker, position: { top: 0, left: 1 } })).toBe(true); - expect(verticalRotationMarkerWithinBounds).toBe(true); + expect(isWithinLineMarkerBounds({ x: 15, y: 15 }, { ...marker, position: { top: 10, left: 10 } })).toBe(true); }); }); diff --git a/src/chart_types/xy_chart/annotations/annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/annotation_tooltip.ts new file mode 100644 index 0000000000..6d5751c398 --- /dev/null +++ b/src/chart_types/xy_chart/annotations/annotation_tooltip.ts @@ -0,0 +1,35 @@ +import { Dimensions } from '../../../utils/dimensions'; + +export function getFinalAnnotationTooltipPosition( + /** the dimensions of the chart parent container */ + container: Dimensions, + chartDimensions: Dimensions, + /** the dimensions of the tooltip container */ + tooltip: Dimensions, + /** the tooltip computed position not adjusted within chart bounds */ + tooltipAnchor: { top: number; left: number }, + padding = 10, +): { + left: string | null; + top: string | null; +} { + let left = 0; + + const annotationXOffset = window.pageXOffset + container.left + chartDimensions.left + tooltipAnchor.left; + if (chartDimensions.left + tooltipAnchor.left + tooltip.width + padding >= container.width) { + left = annotationXOffset - tooltip.width - padding; + } else { + left = annotationXOffset + padding; + } + let top = window.pageYOffset + container.top + chartDimensions.top + tooltipAnchor.top; + if (chartDimensions.top + tooltipAnchor.top + tooltip.height + padding >= container.height) { + top -= tooltip.height + padding; + } else { + top += padding; + } + + return { + left: `${Math.round(left)}px`, + top: `${Math.round(top)}px`, + }; +} diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts b/src/chart_types/xy_chart/annotations/annotation_utils.test.ts index a44d11e45f..4b2c2ebaa5 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.test.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.test.ts @@ -1,5 +1,5 @@ +import React from 'react'; import { - AnnotationDomainType, AnnotationDomainTypes, AnnotationSpec, AxisSpec, @@ -15,7 +15,6 @@ import { ScaleBand } from '../../../utils/scales/scale_band'; import { ScaleContinuous } from '../../../utils/scales/scale_continuous'; import { Scale, ScaleType } from '../../../utils/scales/scales'; import { - AnnotationLinePosition, AnnotationLineProps, computeAnnotationDimensions, computeAnnotationTooltipState, @@ -25,22 +24,18 @@ import { computeRectAnnotationDimensions, computeRectAnnotationTooltipState, computeRectTooltipLeft, - computeRectTooltipOffset, computeRectTooltipTop, - DEFAULT_LINE_OVERFLOW, getAnnotationAxis, - getAnnotationLineTooltipPosition, - getAnnotationLineTooltipTransform, getAnnotationLineTooltipXOffset, getAnnotationLineTooltipYOffset, getRotatedCursor, isBottomRectTooltip, isRightRectTooltip, isVerticalAnnotationLine, - isWithinLineBounds, isWithinRectBounds, scaleAndValidateDatum, - toTransformString, + AnnotationTooltipState, + AnnotationDimensions, } from './annotation_utils'; import { Point } from '../store/chart_state'; @@ -137,12 +132,19 @@ describe('annotation utils', () => { 1, false, ); - const expectedDimensions = new Map(); + const expectedDimensions = new Map(); expectedDimensions.set(annotationId, [ { - position: [DEFAULT_LINE_OVERFLOW, 20, 10, 20], + anchor: { + top: 20, + left: 0, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 0, y1: 20 }, + end: { x2: 10, y2: 20 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, 20, 10, 20], }, ]); expectedDimensions.set(rectAnnotationId, [{ rect: { x: 0, y: 30, width: 25, height: 20 } }]); @@ -211,11 +213,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [DEFAULT_LINE_OVERFLOW, 20, 10, 20], + anchor: { + top: 20, + left: 0, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 0, y1: 20 }, + end: { x2: 10, y2: 20 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, 20, 10, 20], }, ]; expect(dimensions).toEqual(expectedDimensions); @@ -248,11 +257,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [0, 20, 10, 20], + anchor: { + top: 20, + left: 10, + position: Position.Right, + }, + linePathPoints: { + start: { x1: 10, y1: 20 }, + end: { x2: 0, y2: 20 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, 20, 10, 20], }, ]; expect(dimensions).toEqual(expectedDimensions); @@ -285,11 +301,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [0, 20, 20 + DEFAULT_LINE_OVERFLOW, 20], + anchor: { + top: 20, + left: -10, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 20, y1: 20 }, + end: { x2: 0, y2: 20 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [20, 0, 20, 20], marker: undefined, }, ]; @@ -349,11 +372,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [12.5, -DEFAULT_LINE_OVERFLOW, 12.5, 20], + anchor: { + top: 0, + left: 12.5, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 12.5, y1: 0 }, + end: { x2: 12.5, y2: 20 }, + }, details: { detailsText: 'foo', headerText: 'a' }, - tooltipLinePosition: [12.5, 0, 12.5, 20], }, ]; expect(dimensions).toEqual(expectedDimensions); @@ -384,11 +414,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [25, -DEFAULT_LINE_OVERFLOW, 25, 20], + anchor: { + top: 0, + left: 25, + position: Position.Top, + }, + linePathPoints: { + start: { x1: 25, y1: 0 }, + end: { x2: 25, y2: 20 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [25, 0, 25, 20], marker: undefined, }, ]; @@ -420,11 +457,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [25, 0, 25, 20], + anchor: { + top: 20, + left: 25, + position: Position.Bottom, + }, + linePathPoints: { + start: { x1: 25, y1: 20 }, + end: { x2: 25, y2: 0 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [25, 0, 25, 20], marker: undefined, }, ]; @@ -456,11 +500,18 @@ describe('annotation utils', () => { 0, true, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [110, 0, 110, 20], + anchor: { + top: 20, + left: 110, + position: Position.Bottom, + }, + linePathPoints: { + start: { x1: 110, y1: 20 }, + end: { x2: 110, y2: 0 }, + }, details: { detailsText: 'foo', headerText: '10.5' }, - tooltipLinePosition: [110, 0, 110, 20], marker: undefined, }, ]; @@ -493,11 +544,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [12.5, -DEFAULT_LINE_OVERFLOW, 12.5, 10], + anchor: { + top: 12.5, + left: 0, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 12.5, y1: 0 }, + end: { x2: 12.5, y2: 10 }, + }, details: { detailsText: 'foo', headerText: 'a' }, - tooltipLinePosition: [0, 12.5, 10, 12.5], marker: undefined, }, ]; @@ -530,11 +588,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [25, -DEFAULT_LINE_OVERFLOW, 25, 10], + anchor: { + top: 25, + left: 0, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 25, y1: 0 }, + end: { x2: 25, y2: 10 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, 25, 10, 25], marker: undefined, }, ]; @@ -567,11 +632,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [25, -DEFAULT_LINE_OVERFLOW, 25, 10], + anchor: { + top: -5, + left: 0, + position: Position.Left, + }, + linePathPoints: { + start: { x1: 25, y1: 0 }, + end: { x2: 25, y2: 10 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [0, -5, 10, -5], marker: undefined, }, ]; @@ -604,11 +676,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [25, -DEFAULT_LINE_OVERFLOW, 25, 20], + anchor: { + top: 0, + left: -15, + position: Position.Top, + }, + linePathPoints: { + start: { x1: 25, y1: 0 }, + end: { x2: 25, y2: 20 }, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [25, 0, 25, 20], marker: undefined, }, ]; @@ -640,11 +719,18 @@ describe('annotation utils', () => { 0, false, ); - const expectedDimensions = [ + const expectedDimensions: AnnotationLineProps[] = [ { - position: [25, DEFAULT_LINE_OVERFLOW, 25, 20], + anchor: { + top: 20, + left: -15, + position: Position.Bottom, + }, details: { detailsText: 'foo', headerText: '2' }, - tooltipLinePosition: [25, DEFAULT_LINE_OVERFLOW, 25, 20], + linePathPoints: { + start: { x1: 25, y1: 20 }, + end: { x2: 25, y2: 0 }, + }, marker: undefined, }, ]; @@ -815,120 +901,6 @@ describe('annotation utils', () => { expect(hiddenAnnotationDimensions).toEqual(null); }); - test('should compute if a point is within an annotation line bounds (xDomain annotation)', () => { - const linePosition1: AnnotationLinePosition = [10, 0, 10, 20]; - const cursorPosition1: Point = { x: 0, y: 0 }; - const cursorPosition2: Point = { x: 10, y: 0 }; - - const offset = 0; - const horizontalChartRotation: Rotation = 0; - const verticalChartRotation: Rotation = 90; - const domainType: AnnotationDomainType = AnnotationDomainTypes.XDomain; - - const horizontalRotationOutsideBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - cursorPosition1, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - ); - - expect(horizontalRotationOutsideBounds).toBe(false); - - const horizontalRotationWithinBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - cursorPosition2, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - ); - expect(horizontalRotationWithinBounds).toBe(true); - - const verticalRotationOutsideBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - cursorPosition1, - offset, - verticalChartRotation, - chartDimensions, - domainType, - ); - - expect(verticalRotationOutsideBounds).toBe(false); - - const verticalRotationWithinBounds = isWithinLineBounds( - Position.Bottom, - linePosition1, - { x: 0, y: 10 }, - offset, - verticalChartRotation, - chartDimensions, - domainType, - ); - - expect(verticalRotationWithinBounds).toBe(true); - }); - test('should compute if a point is within an annotation line bounds (yDomain annotation)', () => { - const linePosition1: AnnotationLinePosition = [10, 0, 10, 20]; - const cursorPosition1: Point = { x: 0, y: 0 }; - const cursorPosition2: Point = { x: 10, y: 0 }; - - const offset = 0; - const horizontalChartRotation: Rotation = 0; - const verticalChartRotation: Rotation = 90; - const domainType: AnnotationDomainType = AnnotationDomainTypes.YDomain; - - const horizontalRotationOutsideBounds = isWithinLineBounds( - Position.Left, - linePosition1, - cursorPosition1, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - ); - - expect(horizontalRotationOutsideBounds).toBe(false); - - const horizontalRotationWithinBounds = isWithinLineBounds( - Position.Left, - linePosition1, - cursorPosition2, - offset, - horizontalChartRotation, - chartDimensions, - domainType, - ); - expect(horizontalRotationWithinBounds).toBe(true); - - const verticalRotationOutsideBounds = isWithinLineBounds( - Position.Left, - linePosition1, - cursorPosition1, - offset, - verticalChartRotation, - chartDimensions, - domainType, - ); - - expect(verticalRotationOutsideBounds).toBe(false); - - const verticalRotationWithinBounds = isWithinLineBounds( - Position.Left, - [0, 10, 20, 10], - { x: 0, y: 10 }, - offset, - verticalChartRotation, - chartDimensions, - domainType, - ); - - expect(verticalRotationWithinBounds).toBe(true); - }); test('should determine if an annotation line is vertical dependent on domain type & chart rotation', () => { const isHorizontal = true; const isXDomain = true; @@ -994,69 +966,47 @@ describe('annotation utils', () => { const rightVerticalRotationOffset = getAnnotationLineTooltipYOffset(90, Position.Right); expect(rightVerticalRotationOffset).toBe(100); }); - test('should get annotation line tooltip position', () => { - const chartRotation: Rotation = 0; - const linePosition: AnnotationLinePosition = [1, 2, 3, 4]; - - const bottomLineTooltipPosition = getAnnotationLineTooltipPosition(chartRotation, linePosition, Position.Bottom); - expect(bottomLineTooltipPosition).toEqual({ - xPosition: 1, - yPosition: 4, - xOffset: 50, - yOffset: 100, - }); - - const topLineTooltipPosition = getAnnotationLineTooltipPosition(chartRotation, linePosition, Position.Top); - expect(topLineTooltipPosition).toEqual({ xPosition: 1, yPosition: 2, xOffset: 50, yOffset: 0 }); - - const leftLineTooltipPosition = getAnnotationLineTooltipPosition(chartRotation, linePosition, Position.Left); - expect(leftLineTooltipPosition).toEqual({ - xPosition: 1, - yPosition: 4, - xOffset: 0, - yOffset: 50, - }); - - const rightLineTooltipPosition = getAnnotationLineTooltipPosition(chartRotation, linePosition, Position.Right); - expect(rightLineTooltipPosition).toEqual({ - xPosition: 3, - yPosition: 4, - xOffset: 100, - yOffset: 50, - }); - }); - test('should form the string for the position transform given a TransformPoint', () => { - const transformString = toTransformString({ - xPosition: 1, - yPosition: 4, - xOffset: 50, - yOffset: 100, - }); - expect(transformString).toBe('translate(calc(1px - 50%),calc(4px - 100%))'); - }); - test('should get the transform for an annotation line tooltip', () => { - const chartRotation: Rotation = 0; - const linePosition: AnnotationLinePosition = [1, 2, 3, 4]; - const lineTooltipTransform = getAnnotationLineTooltipTransform(chartRotation, linePosition, Position.Bottom); - expect(lineTooltipTransform).toBe('translate(calc(1px - 50%),calc(4px - 100%))'); - }); test('should compute the tooltip state for an annotation line', () => { const cursorPosition: Point = { x: 1, y: 2 }; const annotationLines: AnnotationLineProps[] = [ { - position: [1, 2, 3, 4], + anchor: { + position: Position.Left, + left: 0, + top: 0, + }, + linePathPoints: { + start: { x1: 1, y1: 2 }, + end: { x2: 3, y2: 4 }, + }, details: {}, - tooltipLinePosition: [1, 2, 3, 4], + marker: { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + }, }, { - position: [0, 10, 20, 10], + anchor: { + position: Position.Left, + left: 0, + top: 0, + }, + linePathPoints: { + start: { x1: 0, y1: 10 }, + end: { x2: 20, y2: 10 }, + }, details: {}, - tooltipLinePosition: [0, 10, 20, 10], + marker: { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 20, height: 20 }, + position: { top: 0, left: 0 }, + }, }, ]; - const lineStyle = DEFAULT_ANNOTATION_LINE_STYLE; - const chartRotation: Rotation = 0; const localAxesSpecs = new Map(); // missing annotation axis (xDomain) @@ -1065,19 +1015,10 @@ describe('annotation utils', () => { annotationLines, groupId, AnnotationDomainTypes.XDomain, - lineStyle, - chartRotation, - chartDimensions, localAxesSpecs, ); - const expectedMissingTooltipState = { - isVisible: false, - transform: '', - annotationType: 'line', - }; - - expect(missingTooltipState).toEqual(expectedMissingTooltipState); + expect(missingTooltipState).toEqual({ isVisible: false }); // add axis for xDomain annotation localAxesSpecs.set(horizontalAxisSpec.id, horizontalAxisSpec); @@ -1087,15 +1028,16 @@ describe('annotation utils', () => { annotationLines, groupId, AnnotationDomainTypes.XDomain, - lineStyle, - chartRotation, - chartDimensions, localAxesSpecs, ); const expectedXDomainTooltipState = { isVisible: true, - transform: 'translate(calc(1px - 50%),calc(4px - 100%))', annotationType: 'line', + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, }; expect(xDomainTooltipState).toEqual(expectedXDomainTooltipState); @@ -1106,14 +1048,15 @@ describe('annotation utils', () => { annotationLines, groupId, AnnotationDomainTypes.XDomain, - lineStyle, - 180, - chartDimensions, localAxesSpecs, ); - const expectedXDomainRotatedTooltipState = { + const expectedXDomainRotatedTooltipState: AnnotationTooltipState = { isVisible: true, - transform: 'translate(calc(9px - 50%),calc(4px - 100%))', + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, annotationType: 'line', }; @@ -1127,14 +1070,15 @@ describe('annotation utils', () => { annotationLines, groupId, AnnotationDomainTypes.YDomain, - lineStyle, - chartRotation, - chartDimensions, localAxesSpecs, ); - const expectedYDomainTooltipState = { + const expectedYDomainTooltipState: AnnotationTooltipState = { isVisible: true, - transform: 'translate(calc(1px - 0%),calc(4px - 50%))', + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, annotationType: 'line', }; @@ -1145,32 +1089,34 @@ describe('annotation utils', () => { annotationLines, groupId, AnnotationDomainTypes.YDomain, - lineStyle, - 180, - chartDimensions, localAxesSpecs, ); - const expectedFlippedYDomainTooltipState = { + const expectedFlippedYDomainTooltipState: AnnotationTooltipState = { isVisible: true, - transform: 'translate(calc(1px - 0%),calc(16px - 50%))', + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, annotationType: 'line', }; expect(flippedYDomainTooltipState).toEqual(expectedFlippedYDomainTooltipState); const rotatedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 0, y: 10 }, + { x: 10, y: 10 }, annotationLines, groupId, AnnotationDomainTypes.YDomain, - lineStyle, - 90, - chartDimensions, localAxesSpecs, ); - const expectedRotatedYDomainTooltipState = { + const expectedRotatedYDomainTooltipState: AnnotationTooltipState = { isVisible: true, - transform: 'translate(calc(10px - 50%),calc(10px - 100%))', + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, annotationType: 'line', }; @@ -1193,9 +1139,19 @@ describe('annotation utils', () => { const annotationLines: AnnotationLineProps[] = [ { - position: [1, 2, 3, 4], + anchor: { + position: Position.Left, + top: 0, + left: 0, + }, + linePathPoints: { start: { x1: 1, y1: 2 }, end: { x2: 3, y2: 4 } }, details: {}, - tooltipLinePosition: [1, 2, 3, 4], + marker: { + icon: React.createElement('div'), + color: 'red', + dimension: { width: 10, height: 10 }, + position: { top: 0, left: 0 }, + }, }, ]; const chartRotation: Rotation = 0; @@ -1264,8 +1220,12 @@ describe('annotation utils', () => { const expectedTooltipState = { isVisible: true, - transform: 'translate(calc(1px - 0%),calc(4px - 50%))', annotationType: 'line', + anchor: { + top: 0, + left: 0, + position: Position.Left, + }, }; expect(tooltipState).toEqual(expectedTooltipState); @@ -1293,15 +1253,14 @@ describe('annotation utils', () => { chartDimensions, ); - const expectedRectTooltipState = { + expect(rectTooltipState).toEqual({ isVisible: true, - transform: 'translate(0, 0)', annotationType: 'rectangle', - top: 4, - left: 5, - }; - - expect(rectTooltipState).toEqual(expectedRectTooltipState); + anchor: { + top: 4, + left: 3, + }, + }); annotationRectangle.hideTooltips = true; const rectHideTooltipState = computeAnnotationTooltipState( @@ -1319,16 +1278,16 @@ describe('annotation utils', () => { test('should get associated axis for an annotation', () => { const localAxesSpecs = new Map(); - const noAxis = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain); + const noAxis = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); expect(noAxis).toBe(null); localAxesSpecs.set(horizontalAxisSpec.id, horizontalAxisSpec); localAxesSpecs.set(verticalAxisSpec.id, verticalAxisSpec); - const xAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain); + const xAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); expect(xAnnotationAxisPosition).toEqual(Position.Bottom); - const yAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.YDomain); + const yAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.YDomain, 0); expect(yAnnotationAxisPosition).toEqual(Position.Left); }); test('should not compute rectangle annotation dimensions when no yScale', () => { @@ -1589,87 +1548,18 @@ describe('annotation utils', () => { expect(computeRectTooltipTop(90, isBottomTooltip, xPosition, cursorY, chartHeight)).toBe(4); expect(computeRectTooltipTop(-90, !isBottomTooltip, xPosition, cursorY, chartHeight)).toBe(8); }); - test('should compute rect annotation tooltip offset', () => { - const isRightTooltip = true; - const isBottomTooltip = true; - - // chart rotation 0 - expect(computeRectTooltipOffset(isRightTooltip, isBottomTooltip, 0)).toEqual({ offsetLeft: '0', offsetTop: '0' }); - expect(computeRectTooltipOffset(!isRightTooltip, isBottomTooltip, 0)).toEqual({ - offsetLeft: '-100%', - offsetTop: '0', - }); - expect(computeRectTooltipOffset(isRightTooltip, !isBottomTooltip, 0)).toEqual({ - offsetLeft: '0', - offsetTop: '-100%', - }); - expect(computeRectTooltipOffset(!isRightTooltip, !isBottomTooltip, 0)).toEqual({ - offsetLeft: '-100%', - offsetTop: '-100%', - }); - - // chart rotation 180 - expect(computeRectTooltipOffset(isRightTooltip, isBottomTooltip, 180)).toEqual({ - offsetLeft: '-100%', - offsetTop: '0', - }); - expect(computeRectTooltipOffset(!isRightTooltip, isBottomTooltip, 180)).toEqual({ - offsetLeft: '0', - offsetTop: '0', - }); - expect(computeRectTooltipOffset(isRightTooltip, !isBottomTooltip, 180)).toEqual({ - offsetLeft: '-100%', - offsetTop: '-100%', - }); - expect(computeRectTooltipOffset(!isRightTooltip, !isBottomTooltip, 180)).toEqual({ - offsetLeft: '0', - offsetTop: '-100%', - }); - - // chart rotation 90 - expect(computeRectTooltipOffset(isRightTooltip, isBottomTooltip, 90)).toEqual({ offsetLeft: '0', offsetTop: '0' }); - expect(computeRectTooltipOffset(!isRightTooltip, isBottomTooltip, 90)).toEqual({ - offsetLeft: '-100%', - offsetTop: '0', - }); - expect(computeRectTooltipOffset(isRightTooltip, !isBottomTooltip, 90)).toEqual({ - offsetLeft: '0', - offsetTop: '-100%', - }); - expect(computeRectTooltipOffset(!isRightTooltip, !isBottomTooltip, 90)).toEqual({ - offsetLeft: '-100%', - offsetTop: '-100%', - }); - - // chart rotation -90 - expect(computeRectTooltipOffset(isRightTooltip, isBottomTooltip, -90)).toEqual({ - offsetLeft: '0', - offsetTop: '-100%', - }); - expect(computeRectTooltipOffset(!isRightTooltip, isBottomTooltip, -90)).toEqual({ - offsetLeft: '-100%', - offsetTop: '-100%', - }); - expect(computeRectTooltipOffset(isRightTooltip, !isBottomTooltip, -90)).toEqual({ - offsetLeft: '0', - offsetTop: '0', - }); - expect(computeRectTooltipOffset(!isRightTooltip, !isBottomTooltip, -90)).toEqual({ - offsetLeft: '-100%', - offsetTop: '0', - }); - }); test('should compute tooltip state for rect annotation', () => { const cursorPosition = { x: 3, y: 4 }; const annotationRects = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; const visibleTooltip = computeRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); - const expectedVisibleTooltipState = { + const expectedVisibleTooltipState: AnnotationTooltipState = { isVisible: true, - transform: 'translate(0, 0)', annotationType: 'rectangle', - top: 4, - left: 5, + anchor: { + top: 4, + left: 3, + }, }; expect(visibleTooltip).toEqual(expectedVisibleTooltipState); @@ -1682,12 +1572,12 @@ describe('annotation utils', () => { }); test('should get rotated cursor position', () => { - const rawCursorPosition = { x: 1, y: 2 }; + const cursorPosition = { x: 1, y: 2 }; - expect(getRotatedCursor(rawCursorPosition, chartDimensions, 0)).toEqual(rawCursorPosition); - expect(getRotatedCursor(rawCursorPosition, chartDimensions, 90)).toEqual({ x: 2, y: 1 }); - expect(getRotatedCursor(rawCursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 9 }); - expect(getRotatedCursor(rawCursorPosition, chartDimensions, 180)).toEqual({ x: 9, y: 18 }); + expect(getRotatedCursor(cursorPosition, chartDimensions, 0)).toEqual(cursorPosition); + expect(getRotatedCursor(cursorPosition, chartDimensions, 90)).toEqual({ x: 2, y: 9 }); + expect(getRotatedCursor(cursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 1 }); + expect(getRotatedCursor(cursorPosition, chartDimensions, 180)).toEqual({ x: 9, y: 18 }); }); test('should compute cluster offset', () => { diff --git a/src/chart_types/xy_chart/annotations/annotation_utils.ts b/src/chart_types/xy_chart/annotations/annotation_utils.ts index a03fb5b010..e0cdc4e110 100644 --- a/src/chart_types/xy_chart/annotations/annotation_utils.ts +++ b/src/chart_types/xy_chart/annotations/annotation_utils.ts @@ -24,16 +24,19 @@ import { Point } from '../store/chart_state'; import { computeXScaleOffset, getAxesSpecForSpecId, isHorizontalRotation } from '../store/utils'; export type AnnotationTooltipFormatter = (details?: string) => JSX.Element | null; -export interface AnnotationTooltipState { + +export type AnnotationTooltipState = AnnotationTooltipVisibleState | AnnotationTooltipHiddenState; +export interface AnnotationTooltipVisibleState { + isVisible: true; annotationType: AnnotationType; - isVisible: boolean; header?: string; details?: string; - transform: string; - top?: number; - left?: number; + anchor: { position?: Position; top: number; left: number }; renderTooltip?: AnnotationTooltipFormatter; } +export interface AnnotationTooltipHiddenState { + isVisible: false; +} export interface AnnotationDetails { headerText?: string; detailsText?: string; @@ -41,16 +44,38 @@ export interface AnnotationDetails { export interface AnnotationMarker { icon: JSX.Element; - transform: string; - dimensions: { width: number; height: number }; + position: { top: number; left: number }; + dimension: { width: number; height: number }; color: string; } -export type AnnotationLinePosition = [number, number, number, number]; +/** + * The path points of a line annotation. + */ +export type AnnotationLinePathPoints = { + /** x1,y1 the start point anchored to the linked axis */ + start: { + x1: number; + y1: number; + }; + /** x2,y2 the end point */ + end: { + x2: number; + y2: number; + }; +}; export interface AnnotationLineProps { - position: AnnotationLinePosition; - tooltipLinePosition: AnnotationLinePosition; + /** the position of the start point relative to the Chart */ + anchor: { + position: Position; + top: number; + left: number; + }; + /** + * The path points of a line annotation + */ + linePathPoints: AnnotationLinePathPoints; details: AnnotationDetails; marker?: AnnotationMarker; } @@ -65,104 +90,124 @@ export interface AnnotationRectProps { details?: string; } -interface TransformPosition { - xPosition: number; - yPosition: number; - xOffset: number; - yOffset: number; -} - // TODO: add AnnotationTextProps export type AnnotationDimensions = AnnotationLineProps[] | AnnotationRectProps[]; -export const DEFAULT_LINE_OVERFLOW = 0; - export function computeYDomainLineAnnotationDimensions( dataValues: LineAnnotationDatum[], yScale: Scale, chartRotation: Rotation, - lineOverflow: number, axisPosition: Position, chartDimensions: Dimensions, lineColor: string, marker?: JSX.Element, - markerDimensions?: { width: number; height: number }, + markerDimension = { width: 0, height: 0 }, ): AnnotationLineProps[] { const chartHeight = chartDimensions.height; const chartWidth = chartDimensions.width; const isHorizontalChartRotation = isHorizontalRotation(chartRotation); - const markerOffsets = markerDimensions || { width: 0, height: 0 }; + const lineProps: AnnotationLineProps[] = []; dataValues.forEach((datum: LineAnnotationDatum) => { const { dataValue } = datum; - const details = { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }; - // d3.scale will return 0 for '', rendering the line incorrectly at 0 - if (dataValue === '') { + // avoid rendering invalid annotation value + if (dataValue === null || dataValue === undefined || dataValue === '') { return; } - const scaledYValue = yScale.scale(dataValue); - if (isNaN(scaledYValue)) { + const annotationValueYposition = yScale.scale(dataValue); + // avoid rendering non scalable annotation values + if (isNaN(annotationValueYposition)) { return; } const [domainStart, domainEnd] = yScale.domain; + // avoid rendering annotation with values outside the scale domain if (domainStart > dataValue || domainEnd < dataValue) { return; } - - const yDomainPosition = scaledYValue; - - const leftHorizontalAxis: AnnotationLinePosition = [0 - lineOverflow, yDomainPosition, chartWidth, yDomainPosition]; - const rightHorizontaAxis: AnnotationLinePosition = [0, yDomainPosition, chartWidth + lineOverflow, yDomainPosition]; - - // Without overflow applied - const baseLinePosition: AnnotationLinePosition = isHorizontalChartRotation - ? [0, yDomainPosition, chartWidth, yDomainPosition] - : [yDomainPosition, 0, yDomainPosition, chartHeight]; - - const linePosition: AnnotationLinePosition = isHorizontalChartRotation - ? axisPosition === Position.Left - ? leftHorizontalAxis - : rightHorizontaAxis - : [0, yDomainPosition, chartHeight + lineOverflow, yDomainPosition]; - - const markerPosition: AnnotationLinePosition = isHorizontalChartRotation - ? ([...linePosition] as AnnotationLinePosition) - : [yDomainPosition, 0, yDomainPosition, chartHeight + lineOverflow]; - + const anchor = { + position: axisPosition, + top: 0, + left: 0, + }; + const markerPosition = { top: 0, left: 0 }; + const linePathPoints: AnnotationLinePathPoints = { + start: { x1: 0, y1: 0 }, + end: { x2: 0, y2: 0 }, + }; + // the Y axis is vertical, X axis is horizontal y|--x--|y if (isHorizontalChartRotation) { + // y|__x__ if (axisPosition === Position.Left) { - markerPosition[0] -= markerOffsets.width; + anchor.left = 0; + markerPosition.left = -markerDimension.width; + linePathPoints.start.x1 = 0; + linePathPoints.end.x2 = chartWidth; + // __x__|y } else { - markerPosition[2] += markerOffsets.width; + anchor.left = chartWidth; + markerPosition.left = chartWidth; + linePathPoints.start.x1 = chartWidth; + linePathPoints.end.x2 = 0; } - if (chartRotation === 180) { - markerPosition[1] = chartHeight - markerPosition[1]; - markerPosition[3] = chartHeight - markerPosition[3]; + // __x__ + if (chartRotation === 0) { + anchor.top = annotationValueYposition; + markerPosition.top = annotationValueYposition - markerDimension.height / 2; + // ¯¯x¯¯ + } else { + anchor.top = chartHeight - annotationValueYposition; + markerPosition.top = chartHeight - annotationValueYposition - markerDimension.height / 2; } + linePathPoints.start.y1 = annotationValueYposition; + linePathPoints.end.y2 = annotationValueYposition; + // the Y axis is horizontal, X axis is vertical x|--y--|x } else { - markerPosition[3] += markerOffsets.height; - if (chartRotation === 90) { - markerPosition[0] = chartWidth - markerPosition[0]; - markerPosition[2] = chartWidth - markerPosition[2]; + // ¯¯y¯¯ + if (axisPosition === Position.Top) { + anchor.top = 0; + markerPosition.top = -markerDimension.height; + linePathPoints.start.x1 = 0; + linePathPoints.end.x2 = chartHeight; + // __y__ + } else { + anchor.top = chartHeight; + markerPosition.top = chartHeight; + linePathPoints.start.x1 = chartHeight; + linePathPoints.end.x2 = 0; } + // __y__|x + if (chartRotation === -90) { + anchor.left = annotationValueYposition; + markerPosition.left = annotationValueYposition - markerDimension.width / 2; + // x|__y__ + } else { + anchor.left = chartWidth - annotationValueYposition; + markerPosition.left = chartWidth - annotationValueYposition - markerDimension.width / 2; + } + linePathPoints.start.y1 = annotationValueYposition; + linePathPoints.end.y2 = annotationValueYposition; } - const markerTransform = getAnnotationLineTooltipTransform(chartRotation, markerPosition, axisPosition); - const annotationMarker = marker - ? { icon: marker, transform: markerTransform, color: lineColor, dimensions: markerOffsets } + const annotationMarker: AnnotationMarker | undefined = marker + ? { + icon: marker, + color: lineColor, + dimension: markerDimension, + position: markerPosition, + } : undefined; - const lineProp = { - position: linePosition, - details, + const lineProp: AnnotationLineProps = { + anchor, + linePathPoints, marker: annotationMarker, - tooltipLinePosition: baseLinePosition, + details: { + detailsText: datum.details, + headerText: datum.header || dataValue.toString(), + }, }; lineProps.push(lineProp); @@ -175,29 +220,22 @@ export function computeXDomainLineAnnotationDimensions( dataValues: LineAnnotationDatum[], xScale: Scale, chartRotation: Rotation, - lineOverflow: number, axisPosition: Position, chartDimensions: Dimensions, lineColor: string, xScaleOffset: number, enableHistogramMode: boolean, marker?: JSX.Element, - markerDimensions?: { width: number; height: number }, + markerDimension = { width: 0, height: 0 }, ): AnnotationLineProps[] { const chartHeight = chartDimensions.height; const chartWidth = chartDimensions.width; - const markerOffsets = markerDimensions || { width: 0, height: 0 }; const lineProps: AnnotationLineProps[] = []; + const isHorizontalChartRotation = isHorizontalRotation(chartRotation); const alignWithTick = xScale.bandwidth > 0 && !enableHistogramMode; dataValues.forEach((datum: LineAnnotationDatum) => { const { dataValue } = datum; - const details = { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }; - - const offset = xScale.bandwidth / 2 - xScaleOffset; const scaledXValue = scaleAndValidateDatum(dataValue, xScale, alignWithTick); @@ -205,60 +243,92 @@ export function computeXDomainLineAnnotationDimensions( return; } - const xDomainPosition = scaledXValue + offset; - - let linePosition: AnnotationLinePosition = [0, 0, 0, 0]; - let tooltipLinePosition: AnnotationLinePosition = [0, 0, 0, 0]; - let markerPosition: AnnotationLinePosition = [0, 0, 0, 0]; - - switch (chartRotation) { - case 0: { - const startY = axisPosition === Position.Bottom ? 0 : -lineOverflow; - const endY = axisPosition === Position.Bottom ? chartHeight + lineOverflow : chartHeight; - linePosition = [xDomainPosition, startY, xDomainPosition, endY]; - tooltipLinePosition = [xDomainPosition, 0, xDomainPosition, chartHeight]; + const offset = xScale.bandwidth / 2 - xScaleOffset; + const annotationValueXposition = scaledXValue + offset; - const startMarkerY = axisPosition === Position.Bottom ? 0 : -lineOverflow - markerOffsets.height; - const endMarkerY = - axisPosition === Position.Bottom ? chartHeight + lineOverflow + markerOffsets.height : chartHeight; - markerPosition = [xDomainPosition, startMarkerY, xDomainPosition, endMarkerY]; - break; + const markerPosition = { top: 0, left: 0 }; + const linePathPoints: AnnotationLinePathPoints = { + start: { x1: 0, y1: 0 }, + end: { x2: 0, y2: 0 }, + }; + const anchor = { + position: axisPosition, + top: 0, + left: 0, + }; + // the Y axis is vertical, X axis is horizontal y|--x--|y + if (isHorizontalChartRotation) { + // __x__ + if (axisPosition === Position.Bottom) { + linePathPoints.start.y1 = chartHeight; + linePathPoints.end.y2 = 0; + anchor.top = chartHeight; + markerPosition.top = chartHeight; + // ¯¯x¯¯ + } else { + linePathPoints.start.y1 = 0; + linePathPoints.end.y2 = chartHeight; + anchor.top = 0; + markerPosition.top = 0 - markerDimension.height; } - case 90: { - linePosition = [xDomainPosition, -lineOverflow, xDomainPosition, chartWidth]; - tooltipLinePosition = [0, xDomainPosition, chartWidth, xDomainPosition]; - - const markerStartX = -lineOverflow - markerOffsets.width; - markerPosition = [markerStartX, xDomainPosition, chartWidth, xDomainPosition]; - break; + // __x__ + if (chartRotation === 0) { + anchor.left = annotationValueXposition; + markerPosition.left = annotationValueXposition - markerDimension.width / 2; + // ¯¯x¯¯ + } else { + anchor.left = chartWidth - annotationValueXposition; + markerPosition.left = chartWidth - annotationValueXposition - markerDimension.width / 2; } - case -90: { - linePosition = [xDomainPosition, -lineOverflow, xDomainPosition, chartWidth]; - tooltipLinePosition = [0, chartHeight - xDomainPosition, chartWidth, chartHeight - xDomainPosition]; - - const markerStartX = -lineOverflow - markerOffsets.width; - markerPosition = [markerStartX, chartHeight - xDomainPosition, chartWidth, chartHeight - xDomainPosition]; - break; + linePathPoints.start.x1 = annotationValueXposition; + linePathPoints.end.x2 = annotationValueXposition; + // the Y axis is horizontal, X axis is vertical x|--y--|x + } else { + // x|--y-- + if (axisPosition === Position.Left) { + anchor.left = 0; + markerPosition.left = -markerDimension.width; + linePathPoints.start.x1 = annotationValueXposition; + linePathPoints.end.x2 = annotationValueXposition; + // --y--|x + } else { + anchor.left = chartWidth; + markerPosition.left = chartWidth; + linePathPoints.start.x1 = annotationValueXposition; + linePathPoints.end.x2 = annotationValueXposition; } - case 180: { - const startY = axisPosition === Position.Bottom ? 0 : -lineOverflow; - const endY = axisPosition === Position.Bottom ? chartHeight + lineOverflow : chartHeight; - linePosition = [xDomainPosition, startY, xDomainPosition, endY]; - tooltipLinePosition = [xDomainPosition, 0, xDomainPosition, chartHeight]; - - const startMarkerY = axisPosition === Position.Bottom ? 0 : -lineOverflow - markerOffsets.height; - const endMarkerY = - axisPosition === Position.Bottom ? chartHeight + lineOverflow + markerOffsets.height : chartHeight; - markerPosition = [chartWidth - xDomainPosition, startMarkerY, chartWidth - xDomainPosition, endMarkerY]; - break; + // __y__|x + if (chartRotation === -90) { + anchor.top = chartHeight - annotationValueXposition; + markerPosition.top = chartHeight - annotationValueXposition - markerDimension.height / 2; + linePathPoints.start.y1 = 0; + linePathPoints.end.y2 = chartWidth; + // x|__y__ + } else { + anchor.top = annotationValueXposition; + markerPosition.top = annotationValueXposition - markerDimension.height / 2; + linePathPoints.start.y1 = 0; + linePathPoints.end.y2 = chartWidth; } } - const markerTransform = getAnnotationLineTooltipTransform(chartRotation, markerPosition, axisPosition); - const annotationMarker = marker - ? { icon: marker, transform: markerTransform, color: lineColor, dimensions: markerOffsets } + const annotationMarker: AnnotationMarker | undefined = marker + ? { + icon: marker, + color: lineColor, + dimension: markerDimension, + position: markerPosition, + } : undefined; - const lineProp = { position: linePosition, details, marker: annotationMarker, tooltipLinePosition }; + const lineProp: AnnotationLineProps = { + anchor, + linePathPoints, + details: { + detailsText: datum.details, + headerText: datum.header || dataValue.toString(), + }, + marker: annotationMarker, + }; lineProps.push(lineProp); }); @@ -281,9 +351,6 @@ export function computeLineAnnotationDimensions( return null; } - // TODO : make line overflow configurable via prop - const lineOverflow = DEFAULT_LINE_OVERFLOW; - // this type is guaranteed as this has been merged with default const lineStyle = annotationSpec.style as LineAnnotationStyle; const lineColor = lineStyle.line.stroke; @@ -293,7 +360,6 @@ export function computeLineAnnotationDimensions( dataValues, xScale, chartRotation, - lineOverflow, axisPosition, chartDimensions, lineColor, @@ -314,7 +380,6 @@ export function computeLineAnnotationDimensions( dataValues, yScale, chartRotation, - lineOverflow, axisPosition, chartDimensions, lineColor, @@ -323,7 +388,7 @@ export function computeLineAnnotationDimensions( ); } -export function scaleAndValidateDatum(dataValue: any, scale: Scale, alignWithTick: boolean): any | null { +export function scaleAndValidateDatum(dataValue: any, scale: Scale, alignWithTick: boolean): number | null { const isContinuous = scale.type !== ScaleType.Ordinal; const scaledValue = scale.scale(dataValue); // d3.scale will return 0 for '', rendering the line incorrectly at 0 @@ -403,7 +468,7 @@ export function computeRectAnnotationDimensions( const y1Scaled = scaleAndValidateDatum(y1, yScale, false); // TODO: surface this as a warning - if ([x0Scaled, x1Scaled, y0Scaled, y1Scaled].includes(null)) { + if (x0Scaled === null || x1Scaled === null || y0Scaled === null || y1Scaled === null) { return; } @@ -448,13 +513,14 @@ export function getAnnotationAxis( axesSpecs: Map, groupId: GroupId, domainType: AnnotationDomainType, + chartRotation: Rotation, ): Position | null { const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); - + const isHorizontalRotated = isHorizontalRotation(chartRotation); const isXDomainAnnotation = isXDomain(domainType); const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; - - return annotationAxis ? annotationAxis.position : null; + const rotatedAnnotation = isHorizontalRotated ? annotationAxis : isXDomainAnnotation ? yAxis : xAxis; + return rotatedAnnotation ? rotatedAnnotation.position : null; } export function computeClusterOffset(totalBarsInCluster: number, barsShift: number, bandwidth: number): number { @@ -490,12 +556,11 @@ export function computeAnnotationDimensions( annotations.forEach((annotationSpec: AnnotationSpec, annotationId: AnnotationId) => { if (isLineAnnotation(annotationSpec)) { const { groupId, domainType } = annotationSpec; - const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType); + const annotationAxisPosition = getAnnotationAxis(axesSpecs, groupId, domainType, chartRotation); if (!annotationAxisPosition) { return; } - const dimensions = computeLineAnnotationDimensions( annotationSpec, chartDimensions, @@ -528,103 +593,16 @@ export function computeAnnotationDimensions( return annotationDimensions; } -export function isWithinLineBounds( - axisPosition: Position, - linePosition: AnnotationLinePosition, - rawCursorPosition: Point, - offset: number, - chartRotation: Rotation, - chartDimensions: Dimensions, - domainType: AnnotationDomainType, - marker?: AnnotationMarker, - hideLinesTooltips?: boolean, -): boolean { - const [startX, startY, endX, endY] = linePosition; - const isXDomainAnnotation = isXDomain(domainType); - const cursorPosition = getRotatedCursor(rawCursorPosition, chartDimensions, chartRotation); - - let isCursorWithinXBounds = false; - let isCursorWithinYBounds = false; - - const isHorizontalChartRotation = isHorizontalRotation(chartRotation); - const chartWidth = chartDimensions.width; - const chartHeight = chartDimensions.height; - if (!hideLinesTooltips) { - if (isXDomainAnnotation) { - isCursorWithinXBounds = isHorizontalChartRotation - ? cursorPosition.x >= startX - offset && cursorPosition.x <= endX + offset - : cursorPosition.x >= chartHeight - startX - offset && cursorPosition.x <= chartHeight - endX + offset; - isCursorWithinYBounds = isHorizontalChartRotation - ? cursorPosition.y >= startY && cursorPosition.y <= endY - : cursorPosition.y >= startY - offset && cursorPosition.y <= endY + offset; - } else { - isCursorWithinXBounds = isHorizontalChartRotation - ? cursorPosition.x >= startX && cursorPosition.x <= endX - : cursorPosition.x >= startX - offset && cursorPosition.x <= endX + offset; - isCursorWithinYBounds = isHorizontalChartRotation - ? cursorPosition.y >= startY - offset && cursorPosition.y <= endY + offset - : cursorPosition.y >= chartWidth - startY - offset && cursorPosition.y <= chartWidth - endY + offset; - } - // If it's within cursor bounds, return true (no need to check marker bounds) - if (isCursorWithinXBounds && isCursorWithinYBounds) { - return true; - } - } - - if (!marker) { - return false; - } - - // Check if cursor within marker bounds - let isCursorWithinMarkerXBounds = false; - let isCursorWithinMarkerYBounds = false; - - const markerWidth = marker.dimensions.width; - const markerHeight = marker.dimensions.height; - const markerWidthOffset = offset + markerWidth / 2; - const markerHeightOffset = offset + markerHeight / 2; - - if (isXDomainAnnotation) { - const bottomAxisYBounds = - chartRotation === 0 - ? cursorPosition.y <= endY + markerHeight && cursorPosition.y >= endY - : cursorPosition.y >= startY - markerHeight && cursorPosition.y <= startY; - const topAxisYBounds = - chartRotation === 0 - ? cursorPosition.y >= startY - markerHeight && cursorPosition.y <= startY - : cursorPosition.y <= endY + markerHeight && cursorPosition.y >= endY; - - isCursorWithinMarkerXBounds = isHorizontalChartRotation - ? cursorPosition.x <= endX + markerWidthOffset && cursorPosition.x >= startX - markerWidthOffset - : cursorPosition.x >= startX - markerWidthOffset && cursorPosition.x <= startX + markerWidthOffset; - isCursorWithinMarkerYBounds = isHorizontalChartRotation - ? axisPosition === Position.Top - ? topAxisYBounds - : bottomAxisYBounds - : cursorPosition.y >= startY - markerHeightOffset && cursorPosition.y <= endY + markerHeightOffset; - } else { - const leftAxisXBounds = - chartRotation === 0 - ? cursorPosition.x >= startX - markerWidth && cursorPosition.x <= startX - : cursorPosition.x <= endX + markerWidth && cursorPosition.x >= endX; - - const rightAxisXBounds = - chartRotation === 0 - ? cursorPosition.x <= endX + markerWidth && cursorPosition.x >= endX - : cursorPosition.x >= startX - markerWidth && cursorPosition.x <= startX; - - isCursorWithinMarkerXBounds = isHorizontalChartRotation - ? axisPosition === Position.Right - ? rightAxisXBounds - : leftAxisXBounds - : cursorPosition.x <= endX + offset + markerWidth && cursorPosition.x >= startX - offset - markerWidth; - isCursorWithinMarkerYBounds = isHorizontalChartRotation - ? cursorPosition.y >= startY - markerHeightOffset && cursorPosition.y <= endY + markerHeightOffset - : cursorPosition.y >= chartWidth - startY - markerHeightOffset && - cursorPosition.y <= chartWidth - endY + markerHeightOffset; - } - - return isCursorWithinMarkerXBounds && isCursorWithinMarkerYBounds; +/** + * Checks if the cursorPosition is within the line annotation marker + * @param cursorPosition the cursor position relative to the projected area + * @param marker the line annotation marker + */ +export function isWithinLineMarkerBounds(cursorPosition: Point, marker: AnnotationMarker): boolean { + const { top, left } = marker.position; + const { width, height } = marker.dimension; + const markerRect = { startX: left, startY: top, endX: left + width, endY: top + height }; + return isWithinRectBounds(cursorPosition, markerRect); } export function isVerticalAnnotationLine(isXDomainAnnotation: boolean, isHorizontalChartRotation: boolean): boolean { @@ -661,41 +639,6 @@ export function getAnnotationLineTooltipYOffset(chartRotation: Rotation, axisPos return yOffset; } -export function getAnnotationLineTooltipPosition( - chartRotation: Rotation, - linePosition: AnnotationLinePosition, - axisPosition: Position, -): TransformPosition { - const [startX, startY, endX, endY] = linePosition; - - const xPosition = axisPosition === Position.Right ? endX : startX; - const yPosition = axisPosition === Position.Top ? startY : endY; - - const xOffset = getAnnotationLineTooltipXOffset(chartRotation, axisPosition); - const yOffset = getAnnotationLineTooltipYOffset(chartRotation, axisPosition); - - return { xPosition, yPosition, xOffset, yOffset }; -} - -export function toTransformString(position: TransformPosition): string { - const { xPosition, yPosition, xOffset, yOffset } = position; - - const xTranslation = `calc(${xPosition}px - ${xOffset}%)`; - const yTranslation = `calc(${yPosition}px - ${yOffset}%)`; - - return `translate(${xTranslation},${yTranslation})`; -} - -export function getAnnotationLineTooltipTransform( - chartRotation: Rotation, - linePosition: AnnotationLinePosition, - axisPosition: Position, -): string { - const position = getAnnotationLineTooltipPosition(chartRotation, linePosition, axisPosition); - - return toTransformString(position); -} - export function isXDomain(domainType: AnnotationDomainType): boolean { return domainType === AnnotationDomainTypes.XDomain; } @@ -705,115 +648,46 @@ export function computeLineAnnotationTooltipState( annotationLines: AnnotationLineProps[], groupId: GroupId, domainType: AnnotationDomainType, - style: LineAnnotationStyle, - chartRotation: Rotation, - chartDimensions: Dimensions, axesSpecs: Map, - hideLinesTooltips?: boolean, ): AnnotationTooltipState { - const annotationTooltipState: AnnotationTooltipState = { - isVisible: false, - transform: '', - annotationType: AnnotationTypes.Line, - }; - const { xAxis, yAxis } = getAxesSpecForSpecId(axesSpecs, groupId); const isXDomainAnnotation = isXDomain(domainType); const annotationAxis = isXDomainAnnotation ? xAxis : yAxis; - const chartWidth = chartDimensions.width; - const chartHeight = chartDimensions.height; if (!annotationAxis) { - return annotationTooltipState; + return { + isVisible: false, + }; } - const axisPosition = annotationAxis.position; - - annotationLines.forEach((line: AnnotationLineProps) => { - const lineOffset = style.line.strokeWidth / 2; - const isWithinBounds = isWithinLineBounds( - axisPosition, - line.position, - cursorPosition, - lineOffset, - chartRotation, - chartDimensions, - domainType, - line.marker, - hideLinesTooltips, - ); + const totalAnnotationLines = annotationLines.length; + for (let i = 0; i < totalAnnotationLines; i++) { + const line = annotationLines[i]; + const isWithinBounds = line.marker && isWithinLineMarkerBounds(cursorPosition, line.marker); if (isWithinBounds) { - annotationTooltipState.isVisible = true; - - // Position tooltip based on axis position & lineOffset amount - const [tooltipStartX, tooltipStartY, tooltipEndX, tooltipEndY] = line.tooltipLinePosition; - const tooltipLinePosition: AnnotationLinePosition = [tooltipStartX, tooltipStartY, tooltipEndX, tooltipEndY]; - - annotationTooltipState.transform = getAnnotationLineTooltipTransform( - chartRotation, - tooltipLinePosition, - axisPosition, - ); - - if (chartRotation === 180 && domainType === AnnotationDomainTypes.YDomain) { - const flippedYDomainTooltipLinePosition: AnnotationLinePosition = [ - tooltipStartX, - chartHeight - tooltipStartY, - tooltipEndX, - chartHeight - tooltipEndY, - ]; - - annotationTooltipState.transform = getAnnotationLineTooltipTransform( - chartRotation, - flippedYDomainTooltipLinePosition, - axisPosition, - ); - } - if (chartRotation === 180 && domainType === AnnotationDomainTypes.XDomain) { - const rotatedXDomainTooltipLinePosition: AnnotationLinePosition = [ - chartWidth - tooltipStartX, - tooltipStartY, - chartWidth - tooltipEndX, - tooltipEndY, - ]; - annotationTooltipState.transform = getAnnotationLineTooltipTransform( - chartRotation, - rotatedXDomainTooltipLinePosition, - axisPosition, - ); - } - if (chartRotation === 90 && domainType === AnnotationDomainTypes.YDomain) { - const rotatedYDomainTooltipLinePosition: AnnotationLinePosition = [ - chartWidth - tooltipStartX, - tooltipStartY, - chartWidth - tooltipEndX, - tooltipEndY, - ]; - - annotationTooltipState.transform = getAnnotationLineTooltipTransform( - chartRotation, - rotatedYDomainTooltipLinePosition, - axisPosition, - ); - } - - if (line.details) { - annotationTooltipState.header = line.details.headerText; - annotationTooltipState.details = line.details.detailsText; - } + return { + annotationType: AnnotationTypes.Line, + isVisible: true, + anchor: { + ...line.anchor, + }, + ...(line.details && { header: line.details.headerText }), + ...(line.details && { details: line.details.detailsText }), + }; } - }); - - return annotationTooltipState; + } + return { + isVisible: false, + }; } export function isWithinRectBounds( cursorPosition: Point, { startX, endX, startY, endY }: { startX: number; endX: number; startY: number; endY: number }, ): boolean { - const withinXBounds = cursorPosition.x > startX && cursorPosition.x < endX; - const withinYBounds = cursorPosition.y > startY && cursorPosition.y < endY; + const withinXBounds = cursorPosition.x >= startX && cursorPosition.x <= endX; + const withinYBounds = cursorPosition.y >= startY && cursorPosition.y <= endY; return withinXBounds && withinYBounds; } @@ -854,55 +728,39 @@ export function computeRectTooltipTop( return isHorizontalChartRotation ? cursorY : chartRotation === -90 ? chartHeight - verticalTop : verticalTop; } -export function computeRectTooltipOffset( - isRightTooltip: boolean, - isBottomTooltip: boolean, - chartRotation: Rotation, -): { offsetLeft: string; offsetTop: string } { - const offsetLeft = isRightTooltip ? (chartRotation === 180 ? '-100%' : '0') : chartRotation === 180 ? '0' : '-100%'; - const offsetTop = isBottomTooltip ? (chartRotation === -90 ? '-100%' : '0') : chartRotation === -90 ? '0' : '-100%'; - - return { offsetLeft, offsetTop }; -} - export function getRotatedCursor( - rawCursorPosition: Point, + /** the cursor position relative to the projection area */ + cursorPosition: Point, chartDimensions: Dimensions, chartRotation: Rotation, ): Point { - const { x, y } = rawCursorPosition; + const { x, y } = cursorPosition; const { height, width } = chartDimensions; switch (chartRotation) { case 0: return { x, y }; case 90: - return { x: y, y: x }; + return { x: y, y: width - x }; case -90: - return { x: height - y, y: width - x }; + return { x: height - y, y: x }; case 180: return { x: width - x, y: height - y }; } } export function computeRectAnnotationTooltipState( - rawCursorPosition: Point, + /** the cursor position relative to the projection area */ + cursorPosition: Point, annotationRects: AnnotationRectProps[], chartRotation: Rotation, chartDimensions: Dimensions, renderTooltip?: AnnotationTooltipFormatter, ): AnnotationTooltipState { - const cursorPosition = getRotatedCursor(rawCursorPosition, chartDimensions, chartRotation); + const rotatedCursorPosition = getRotatedCursor(cursorPosition, chartDimensions, chartRotation); - const annotationTooltipState: AnnotationTooltipState = { - isVisible: false, - transform: '', - annotationType: AnnotationTypes.Rectangle, - }; - - const isRightTooltip = isRightRectTooltip(chartRotation, cursorPosition, chartDimensions.width); - const isBottomTooltip = isBottomRectTooltip(chartRotation, cursorPosition, chartDimensions.height); - - annotationRects.forEach((rectProps: AnnotationRectProps) => { + const totalAnnotationRect = annotationRects.length; + for (let i = 0; i < totalAnnotationRect; i++) { + const rectProps = annotationRects[i]; const { rect, details } = rectProps; const startX = rect.x; const endX = startX + rect.width; @@ -910,36 +768,23 @@ export function computeRectAnnotationTooltipState( const startY = rect.y; const endY = startY + rect.height; - const isWithinBounds = isWithinRectBounds(cursorPosition, { startX, endX, startY, endY }); + const isWithinBounds = isWithinRectBounds(rotatedCursorPosition, { startX, endX, startY, endY }); if (isWithinBounds) { - annotationTooltipState.isVisible = true; - annotationTooltipState.details = details; - - const tooltipLeft = computeRectTooltipLeft( - chartRotation, - isRightTooltip, - { startX, endX }, - rawCursorPosition.x, - chartDimensions.width, - ); - const tooltipTop = computeRectTooltipTop( - chartRotation, - isBottomTooltip, - { startX, endX }, - rawCursorPosition.y, - chartDimensions.height, - ); - - const { offsetLeft, offsetTop } = computeRectTooltipOffset(isRightTooltip, isBottomTooltip, chartRotation); - - annotationTooltipState.top = tooltipTop; - annotationTooltipState.left = tooltipLeft; - annotationTooltipState.transform = `translate(${offsetLeft}, ${offsetTop})`; - annotationTooltipState.renderTooltip = renderTooltip; + return { + isVisible: true, + annotationType: AnnotationTypes.Rectangle, + anchor: { + left: rotatedCursorPosition.x, + top: rotatedCursorPosition.y, + }, + ...(details && { details }), + ...(renderTooltip && { renderTooltip }), + }; } - }); - - return annotationTooltipState; + } + return { + isVisible: false, + }; } export function computeAnnotationTooltipState( @@ -963,17 +808,12 @@ export function computeAnnotationTooltipState( if (spec.hideLines) { continue; } - const lineAnnotationTooltipState = computeLineAnnotationTooltipState( cursorPosition, annotationDimension, groupId, spec.domainType, - spec.style as LineAnnotationStyle, // this type is guaranteed as this has been merged with default - chartRotation, - chartDimensions, axesSpecs, - spec.hideLinesTooltips, ); if (lineAnnotationTooltipState.isVisible) { diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts new file mode 100644 index 0000000000..dbe6e52b22 --- /dev/null +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts @@ -0,0 +1,180 @@ +import { getFinalTooltipPosition } from './crosshair_utils'; + +describe('Tooltip position', () => { + const container = { + width: 100, + height: 100, + top: 10, + left: 10, + }; + const tooltip = { + width: 40, + height: 30, + top: 0, + left: 0, + }; + describe('horizontal rotated chart', () => { + it('can position the tooltip on the top left corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: true, + vPosition: { + bandHeight: 0, + bandTop: 0, + }, + hPosition: { + bandWidth: 0, + bandLeft: 10, + }, + }, + 5, + ); + expect(position.left).toBe('25px'); + expect(position.top).toBe('10px'); + }); + it('can position the tooltip on the bottom left corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: true, + vPosition: { + bandHeight: 0, + bandTop: 90, + }, + hPosition: { + bandWidth: 0, + bandLeft: 10, + }, + }, + 5, + ); + expect(position.left).toBe('25px'); + expect(position.top).toBe('80px'); + }); + it('can position the tooltip on the top right corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: true, + vPosition: { + bandHeight: 0, + bandTop: 0, + }, + hPosition: { + bandWidth: 0, + bandLeft: 100, + }, + }, + 5, + ); + expect(position.left).toBe('65px'); + expect(position.top).toBe('10px'); + }); + it('can position the tooltip on the bottom right corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: true, + vPosition: { + bandHeight: 0, + bandTop: 90, + }, + hPosition: { + bandWidth: 0, + bandLeft: 100, + }, + }, + 5, + ); + expect(position.left).toBe('65px'); + expect(position.top).toBe('80px'); + }); + }); + describe('vertical rotated chart', () => { + it('can position the tooltip on the top left corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: false, + vPosition: { + bandHeight: 0, + bandTop: 0, + }, + hPosition: { + bandWidth: 0, + bandLeft: 10, + }, + }, + 5, + ); + expect(position.left).toBe('20px'); + expect(position.top).toBe('15px'); + }); + it('can position the tooltip on the bottom left corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: false, + vPosition: { + bandHeight: 0, + bandTop: 90, + }, + hPosition: { + bandWidth: 0, + bandLeft: 10, + }, + }, + 5, + ); + expect(position.left).toBe('20px'); + expect(position.top).toBe('65px'); + }); + it('can position the tooltip on the top right corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: false, + vPosition: { + bandHeight: 0, + bandTop: 0, + }, + hPosition: { + bandWidth: 0, + bandLeft: 100, + }, + }, + 5, + ); + expect(position.left).toBe('70px'); + expect(position.top).toBe('15px'); + }); + it('can position the tooltip on the bottom right corner', () => { + const position = getFinalTooltipPosition( + container, + tooltip, + { + isRotatedHorizontal: false, + vPosition: { + bandHeight: 0, + bandTop: 90, + }, + hPosition: { + bandWidth: 0, + bandLeft: 100, + }, + }, + 5, + ); + expect(position.left).toBe('70px'); + expect(position.top).toBe('65px'); + }); + }); +}); diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts index b533f292f5..7b72cb6c0e 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts @@ -8,6 +8,22 @@ export interface SnappedPosition { position: number; band: number; } +export interface TooltipPosition { + /** true if the x axis is horizontal */ + isRotatedHorizontal: boolean; + vPosition: { + /** the top position of the tooltip relative to the parent */ + bandTop: number; + /** the height of the crosshair band if any */ + bandHeight: number; + }; + hPosition: { + /** the left position of the tooltip relative to the parent */ + bandLeft: number; + /** the width of the crosshair band if any */ + bandWidth: number; + }; +} export const DEFAULT_SNAP_POSITION_BAND = 1; @@ -148,7 +164,7 @@ export function getTooltipPosition( cursorBandPosition: Dimensions, cursorPosition: { x: number; y: number }, isSingleValueXScale: boolean, -): string { +): TooltipPosition { const isHorizontalRotated = isHorizontalRotation(chartRotation); const hPosition = getHorizontalTooltipPosition( cursorPosition.x, @@ -164,9 +180,11 @@ export function getTooltipPosition( isHorizontalRotated, isSingleValueXScale, ); - const xTranslation = `translateX(${hPosition.position}px) translateX(-${hPosition.offset}%)`; - const yTranslation = `translateY(${vPosition.position}px) translateY(-${vPosition.offset}%)`; - return `${xTranslation} ${yTranslation}`; + return { + isRotatedHorizontal: isHorizontalRotated, + vPosition, + hPosition, + }; } export function getHorizontalTooltipPosition( @@ -175,39 +193,17 @@ export function getHorizontalTooltipPosition( chartDimensions: Dimensions, isHorizontalRotated: boolean, isSingleValueXScale: boolean, - padding = 20, -): { offset: number; position: number } { +): { bandLeft: number; bandWidth: number } { if (isHorizontalRotated) { - if (isSingleValueXScale) { - return { - offset: 0, - position: cursorBandPosition.left, - }; - } - - if (cursorXPosition <= chartDimensions.width / 2) { - return { - offset: 0, - position: cursorBandPosition.left + cursorBandPosition.width + padding, - }; - } else { - return { - offset: 100, - position: cursorBandPosition.left - padding, - }; - } + return { + bandLeft: cursorBandPosition.left, + bandWidth: isSingleValueXScale ? 0 : cursorBandPosition.width, + }; } else { - if (cursorXPosition <= chartDimensions.width / 2) { - return { - offset: 0, - position: chartDimensions.left + cursorXPosition, - }; - } else { - return { - offset: 100, - position: chartDimensions.left + cursorXPosition, - }; - } + return { + bandWidth: 0, + bandLeft: chartDimensions.left + cursorXPosition, + }; } } @@ -217,40 +213,69 @@ export function getVerticalTooltipPosition( chartDimensions: Dimensions, isHorizontalRotated: boolean, isSingleValueXScale: boolean, - padding = 20, ): { - offset: number; - position: number; + bandHeight: number; + bandTop: number; } { if (isHorizontalRotated) { - if (cursorYPosition <= chartDimensions.height / 2) { - return { - offset: 0, - position: cursorYPosition + chartDimensions.top, - }; + return { + bandHeight: 0, + bandTop: cursorYPosition + chartDimensions.top, + }; + } else { + return { + bandHeight: isSingleValueXScale ? 0 : cursorBandPosition.height, + bandTop: cursorBandPosition.top, + }; + } +} + +export function getFinalTooltipPosition( + /** the dimensions of the chart parent container */ + container: Dimensions, + /** the dimensions of the tooltip container */ + tooltip: Dimensions, + /** the tooltip computed position not adjusted within chart bounds */ + tooltipPosition: TooltipPosition, + /** the padding to add between the tooltip position and the final position */ + padding = 10, +): { + left: string | null; + top: string | null; +} { + const { hPosition, vPosition, isRotatedHorizontal: isHorizontalRotated } = tooltipPosition; + let left = 0; + let top = 0; + if (isHorizontalRotated) { + const leftOfBand = window.pageXOffset + container.left + hPosition.bandLeft; + if (hPosition.bandLeft + hPosition.bandWidth + tooltip.width + padding > container.width) { + left = leftOfBand - tooltip.width - padding; + } else { + left = leftOfBand + hPosition.bandWidth + padding; + } + const topOfBand = window.pageYOffset + container.top; + if (vPosition.bandTop + tooltip.height > container.height) { + top = topOfBand + container.height - tooltip.height; } else { - return { - offset: 100, - position: cursorYPosition + chartDimensions.top, - }; + top = topOfBand + vPosition.bandTop; } } else { - if (isSingleValueXScale) { - return { - offset: 0, - position: cursorBandPosition.top, - }; + const leftOfBand = window.pageXOffset + container.left; + if (hPosition.bandLeft + hPosition.bandWidth + tooltip.width > container.width) { + left = leftOfBand + container.width - tooltip.width; + } else { + left = leftOfBand + hPosition.bandLeft + hPosition.bandWidth; } - if (cursorYPosition <= chartDimensions.height / 2) { - return { - offset: 0, - position: cursorBandPosition.top + cursorBandPosition.height + padding, - }; + const topOfBand = window.pageYOffset + container.top + vPosition.bandTop; + if (vPosition.bandTop + vPosition.bandHeight + tooltip.height + padding > container.height) { + top = topOfBand - tooltip.height - padding; } else { - return { - offset: 100, - position: cursorBandPosition.top - padding, - }; + top = topOfBand + vPosition.bandHeight + padding; } } + + return { + left: `${Math.round(left)}px`, + top: `${Math.round(top)}px`, + }; } diff --git a/src/chart_types/xy_chart/specs/line_annotation.tsx b/src/chart_types/xy_chart/specs/line_annotation.tsx index 518de5c72a..7ad35481c7 100644 --- a/src/chart_types/xy_chart/specs/line_annotation.tsx +++ b/src/chart_types/xy_chart/specs/line_annotation.tsx @@ -14,6 +14,7 @@ export class LineAnnotationSpecComponent extends PureComponent { store.setCursorPosition(chartLeft + 99, chartTop + 99); - const expectedTransform = `translateX(${chartLeft}px) translateX(-0%) translateY(109px) translateY(-100%)`; - expect(store.tooltipPosition.transform).toBe(expectedTransform); + expect(store.tooltipPosition.hPosition.bandLeft).toBe(chartLeft); + expect(store.tooltipPosition.vPosition.bandTop).toBe(109); }); test('vertical chart rotation', () => { store.chartRotation = 90; store.setCursorPosition(chartLeft + 99, chartTop + 99); - const expectedTransform = `translateX(109px) translateX(-100%) translateY(${chartTop}px) translateY(-0%)`; - expect(store.tooltipPosition.transform).toBe(expectedTransform); + expect(store.tooltipPosition.hPosition.bandLeft).toBe(109); + expect(store.tooltipPosition.vPosition.bandTop).toBe(chartTop); }); }); describe('can format tooltip values on rotated chart', () => { diff --git a/src/chart_types/xy_chart/store/chart_state.test.ts b/src/chart_types/xy_chart/store/chart_state.test.ts index 3bfa142f65..7d640d9b3e 100644 --- a/src/chart_types/xy_chart/store/chart_state.test.ts +++ b/src/chart_types/xy_chart/store/chart_state.test.ts @@ -1056,10 +1056,11 @@ describe('Chart Store', () => { const expectedRectTooltipState = { isVisible: true, - transform: 'translate(0, 0)', annotationType: AnnotationTypes.Rectangle, - top: 4, - left: 5, + anchor: { + top: store.rawCursorPosition.y - store.chartDimensions.top, + left: store.rawCursorPosition.x - store.chartDimensions.left, + }, }; store.tooltipData.push(unhighlightedTooltipValue); expect(store.annotationTooltipState.get()).toEqual(expectedRectTooltipState); diff --git a/src/chart_types/xy_chart/store/chart_state.ts b/src/chart_types/xy_chart/store/chart_state.ts index 9c0dc63e1d..2b2daaaa01 100644 --- a/src/chart_types/xy_chart/store/chart_state.ts +++ b/src/chart_types/xy_chart/store/chart_state.ts @@ -1,4 +1,4 @@ -import { action, computed, IObservableValue, observable } from 'mobx'; +import { set, action, computed, IObservableValue, observable } from 'mobx'; import * as uuid from 'uuid'; import { @@ -68,7 +68,12 @@ import { computeAnnotationDimensions, computeAnnotationTooltipState, } from '../annotations/annotation_utils'; -import { getCursorBandPosition, getCursorLinePosition, getTooltipPosition } from '../crosshair/crosshair_utils'; +import { + getCursorBandPosition, + getCursorLinePosition, + getTooltipPosition, + TooltipPosition, +} from '../crosshair/crosshair_utils'; import { BrushExtent, computeBrushExtent, @@ -217,11 +222,21 @@ export class ChartStore { tooltipData = observable.array([], { deep: false }); tooltipType = observable.box(DEFAULT_TOOLTIP_TYPE); tooltipSnap = observable.box(DEFAULT_TOOLTIP_SNAP); - tooltipPosition = observable.object<{ transform: string }>({ transform: '' }); + tooltipPosition = observable.object({ + isRotatedHorizontal: true, + vPosition: { + bandTop: 0, + bandHeight: 0, + }, + hPosition: { + bandLeft: 0, + bandWidth: 0, + }, + }); tooltipHeaderFormatter?: TooltipValueFormatter; /** cursorPosition is used by tooltip, so this is a way to expose the position for other uses */ - rawCursorPosition = observable.object<{ x: number; y: number }>({ x: -1, y: -1 }, undefined, { + rawCursorPosition = observable.object<{ x: number; y: number }>({ x: 100, y: 100 }, undefined, { deep: false, }); @@ -279,7 +294,6 @@ export class ChartStore { chartCursor = computed(() => { const { x: xPos, y: yPos } = this.cursorPosition; - if (yPos < 0 || xPos < 0) { return 'default'; } @@ -445,12 +459,15 @@ export class ChartStore { const isSingleValueXScale = this.xScale.isSingleValue(); - this.tooltipPosition.transform = getTooltipPosition( - this.chartDimensions, - this.chartRotation, - this.cursorBandPosition, - this.cursorPosition, - isSingleValueXScale, + set( + this.tooltipPosition, + getTooltipPosition( + this.chartDimensions, + this.chartRotation, + this.cursorBandPosition, + this.cursorPosition, + isSingleValueXScale, + ), ); const tooltipAndHighlight = getTooltipAndHighlightFromXValue( @@ -475,7 +492,10 @@ export class ChartStore { // if there's an annotation rect tooltip & there isn't a single highlighted element, hide const annotationTooltip = this.annotationTooltipState.get(); - const hasRectAnnotationToolip = annotationTooltip && annotationTooltip.annotationType === AnnotationTypes.Rectangle; + const hasRectAnnotationToolip = + annotationTooltip && + annotationTooltip.isVisible && + annotationTooltip.annotationType === AnnotationTypes.Rectangle; if (hasRectAnnotationToolip && highlightedGeometries.length === 0) { this.clearTooltipAndHighlighted(); return; @@ -541,7 +561,7 @@ export class ChartStore { ); // If there's a highlighted chart element tooltip value, don't show annotation tooltip - if (tooltipState && tooltipState.annotationType === AnnotationTypes.Rectangle) { + if (tooltipState && tooltipState.isVisible && tooltipState.annotationType === AnnotationTypes.Rectangle) { for (const tooltipValue of this.tooltipData) { if (tooltipValue.isHighlighted) { return null; @@ -1056,5 +1076,6 @@ export class ChartStore { // https://github.com/elastic/elastic-charts/issues/89 and https://github.com/elastic/elastic-charts/issues/41 this.canDataBeAnimated = false; this.chartInitialized.set(true); + // this.setCursorPosition(100, 100); } } diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index a34837a5a3..eee569dfaa 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -367,7 +367,9 @@ export type LineAnnotationSpec = BaseAnnotationSpec & { }; /** Annotation lines are hidden */ hideLines?: boolean; - /** Hide tooltip when hovering over the line */ + /** Hide tooltip when hovering over the line + * @default true + */ hideLinesTooltips?: boolean; /** z-index of the annotation relative to other elements in the chart * @default 1 diff --git a/src/components/_annotation.scss b/src/components/_annotation.scss index a46d0c264a..ccebc89a7e 100644 --- a/src/components/_annotation.scss +++ b/src/components/_annotation.scss @@ -1,3 +1,7 @@ +#echAnnotationContainerPortal { + position: absolute; + width: 256px; +} .echAnnotation { pointer-events: none; position: absolute; diff --git a/src/components/_tooltip.scss b/src/components/_tooltip.scss index 5e0e379eb9..8999ca7317 100644 --- a/src/components/_tooltip.scss +++ b/src/components/_tooltip.scss @@ -1,18 +1,24 @@ +#echTooltipContainerPortal { + position: absolute; + width: 256px; +} .echTooltip { + position: absolute; @include euiToolTipStyle; @include euiFontSizeXS; - position: absolute; padding: 0; transition: opacity $euiAnimSpeedNormal; pointer-events: none; user-select: none; + margin-bottom: $euiSizeXS; &__list { - margin: $euiSizeXS; + padding: $euiSizeXS; } &__header { @include euiToolTipTitle; + margin-bottom: 0; padding: $euiSizeXS ($euiSizeXS * 2); } @@ -21,6 +27,7 @@ padding: 3px; box-sizing: border-box; border-left: $euiSizeXS solid transparent; + min-width: 1px; } &__label { diff --git a/src/components/annotation_tooltips.tsx b/src/components/annotation_tooltips.tsx index fea3389143..fb7a758cd2 100644 --- a/src/components/annotation_tooltips.tsx +++ b/src/components/annotation_tooltips.tsx @@ -8,49 +8,100 @@ import { AnnotationTooltipFormatter, } from '../chart_types/xy_chart/annotations/annotation_utils'; import { ChartStore } from '../chart_types/xy_chart/store/chart_state'; +import { createPortal } from 'react-dom'; +import { getFinalAnnotationTooltipPosition } from '../chart_types/xy_chart/annotations/annotation_tooltip'; interface AnnotationTooltipProps { chartStore?: ChartStore; + getChartContainerRef: () => React.RefObject; } +const ANNOTATION_CONTAINER_ID = 'echAnnotationContainerPortal'; class AnnotationTooltipComponent extends React.Component { static displayName = 'AnnotationTooltip'; + portalNode: HTMLDivElement | null = null; + tooltipRef: React.RefObject; - renderTooltip() { - const { annotationTooltipState } = this.props.chartStore!; + constructor(props: AnnotationTooltipProps) { + super(props); + this.tooltipRef = React.createRef(); + } + + createPortalNode() { + const container = document.getElementById(ANNOTATION_CONTAINER_ID); + if (container) { + this.portalNode = container as HTMLDivElement; + } else { + this.portalNode = document.createElement('div'); + this.portalNode.id = ANNOTATION_CONTAINER_ID; + document.body.appendChild(this.portalNode); + } + } + componentDidMount() { + this.createPortalNode(); + } + componentDidUpdate() { + // calling on componentDidUpdate because the annotation container can be + // removed by another chart on the same page + this.createPortalNode(); + if (!this.tooltipRef.current) { + return; + } + const { getChartContainerRef } = this.props; + const chartContainerRef = getChartContainerRef(); + if (!chartContainerRef.current) { + return; + } + const { annotationTooltipState, chartDimensions } = this.props.chartStore!; const tooltipState = annotationTooltipState.get(); + if (!tooltipState || !tooltipState.isVisible || !this.portalNode) { + return null; + } + + const chartContainerBBox = chartContainerRef.current.getBoundingClientRect(); + const tooltipBBox = this.tooltipRef.current.getBoundingClientRect(); + const tooltipStyle = getFinalAnnotationTooltipPosition( + chartContainerBBox, + chartDimensions, + tooltipBBox, + tooltipState.anchor, + ); + + this.portalNode.style.left = tooltipStyle.left; + this.portalNode.style.top = tooltipStyle.top; + } + componentWillUnmount() { + if (this.portalNode && this.portalNode.parentNode) { + this.portalNode.parentNode.removeChild(this.portalNode); + } + } + + renderTooltip = () => { + const { annotationTooltipState } = this.props.chartStore!; + const tooltipState = annotationTooltipState.get(); + if (!this.portalNode) { + return null; + } if (!tooltipState || !tooltipState.isVisible) { return
; } - const { transform, details, header } = tooltipState; - const chartDimensions = this.props.chartStore!.chartDimensions; - - const tooltipTop = tooltipState.top; - const tooltipLeft = tooltipState.left; - const top = tooltipTop == null ? chartDimensions.top : chartDimensions.top + tooltipTop; - const left = tooltipLeft == null ? chartDimensions.left : chartDimensions.left + tooltipLeft; - - const position = { - transform, - top, - left, - }; + const { details, header } = tooltipState; switch (tooltipState.annotationType) { case 'line': { - const props = { position, details, header }; - return ; + const props = { details, header }; + return createPortal(, this.portalNode); } case 'rectangle': { - const props = { details, position, customTooltip: tooltipState.renderTooltip }; - return ; + const props = { details, customTooltip: tooltipState.renderTooltip }; + return createPortal(, this.portalNode); } default: return null; } - } + }; renderAnnotationLineMarkers(annotationLines: AnnotationLineProps[], id: AnnotationId): JSX.Element[] { const { chartDimensions } = this.props.chartStore!; @@ -62,13 +113,12 @@ class AnnotationTooltipComponent extends React.Component return; } - const { transform, icon, color } = line.marker; + const { icon, color, position } = line.marker; const style = { color, - transform, - top: chartDimensions.top, - left: chartDimensions.left, + top: chartDimensions.top + position.top, + left: chartDimensions.left + position.left, }; const markerElement = ( @@ -121,12 +171,12 @@ class AnnotationTooltipComponent extends React.Component export const AnnotationTooltip = inject('chartStore')(observer(AnnotationTooltipComponent)); -function RectAnnotationTooltip(props: { +interface RectAnnotationTooltipProps { details?: string; - position: { transform: string; top: number; left: number }; customTooltip?: AnnotationTooltipFormatter; -}) { - const { details, position, customTooltip } = props; +} +function RectAnnotationTooltipRender(props: RectAnnotationTooltipProps, ref: React.Ref) { + const { details, customTooltip } = props; const tooltipContent = customTooltip ? customTooltip(details) : details; if (!tooltipContent) { @@ -134,7 +184,7 @@ function RectAnnotationTooltip(props: { } return ( -
+
{tooltipContent}
@@ -142,16 +192,19 @@ function RectAnnotationTooltip(props: { ); } -function LineAnnotationTooltip(props: { +const RectAnnotationTooltip = React.forwardRef(RectAnnotationTooltipRender); + +interface LineAnnotationTooltipProps { details?: string; header?: string; - position: { transform: string; top: number; left: number }; -}) { - const { details, position, header } = props; +} +function LineAnnotationTooltipRender(props: LineAnnotationTooltipProps, ref: React.Ref) { + const { details, header } = props; return ( -
+

{header}

{details}
); } +const LineAnnotationTooltip = React.forwardRef(LineAnnotationTooltipRender); diff --git a/src/components/chart.tsx b/src/components/chart.tsx index 9498669b3d..f612e20032 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -1,4 +1,4 @@ -import React, { CSSProperties } from 'react'; +import React, { CSSProperties, createRef } from 'react'; import classNames from 'classnames'; import { Provider } from 'mobx-react'; @@ -37,8 +37,10 @@ export class Chart extends React.Component { renderer: 'canvas', }; private chartSpecStore: ChartStore; + private chartContainerRef: React.RefObject; constructor(props: any) { super(props); + this.chartContainerRef = createRef(); this.chartSpecStore = new ChartStore(props.id); this.state = { legendPosition: this.chartSpecStore.legendPosition.get(), @@ -89,6 +91,9 @@ export class Chart extends React.Component { } } } + getChartContainerRef = () => { + return this.chartContainerRef; + }; render() { const { renderer, size, className } = this.props; @@ -106,6 +111,7 @@ export class Chart extends React.Component { className={chartClassNames} data-ech-render-complete={renderComplete} data-ech-render-count={renderCount} + ref={this.chartContainerRef} > {this.props.children} @@ -115,8 +121,8 @@ export class Chart extends React.Component { {// TODO reenable when SVG rendered is aligned with canvas one renderer === 'svg' && } {renderer === 'canvas' && } - - + +
diff --git a/src/components/react_canvas/line_annotation.tsx b/src/components/react_canvas/line_annotation.tsx index 7b06ddf4c9..92540a68fa 100644 --- a/src/components/react_canvas/line_annotation.tsx +++ b/src/components/react_canvas/line_annotation.tsx @@ -17,10 +17,12 @@ export class LineAnnotation extends React.PureComponent { } private renderAnnotationLine = (lineConfig: AnnotationLineProps, i: number) => { const { line } = this.props.lineStyle; - const { position } = lineConfig; - + const { + start: { x1, y1 }, + end: { x2, y2 }, + } = lineConfig.linePathPoints; const lineProps = { - points: position, + points: [x1, y1, x2, y2], ...line, }; diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index cac0c8260a..8cbf965a04 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -253,7 +253,6 @@ class Chart extends React.Component { ); } else if (isRectAnnotation(spec)) { const rectStyle = spec.style as RectAnnotationStyle; - element = ( { } private renderAnnotationRect = (props: AnnotationRectProps, i: number) => { const { x, y, width, height } = props.rect; - const rectProps = { ...this.props.rectStyle, x, diff --git a/src/components/tooltips.tsx b/src/components/tooltips.tsx index 23539f815f..cfc7addacb 100644 --- a/src/components/tooltips.tsx +++ b/src/components/tooltips.tsx @@ -1,15 +1,62 @@ import classNames from 'classnames'; import { inject, observer } from 'mobx-react'; import React from 'react'; +import { createPortal } from 'react-dom'; import { TooltipValue, TooltipValueFormatter } from '../chart_types/xy_chart/utils/interactions'; import { ChartStore } from '../chart_types/xy_chart/store/chart_state'; +import { getFinalTooltipPosition } from '../chart_types/xy_chart/crosshair/crosshair_utils'; interface TooltipProps { chartStore?: ChartStore; + getChartContainerRef: () => React.RefObject; } class TooltipsComponent extends React.Component { static displayName = 'Tooltips'; + portalNode: HTMLDivElement | null = null; + tooltipRef: React.RefObject; + + constructor(props: TooltipProps) { + super(props); + this.tooltipRef = React.createRef(); + } + createPortalNode() { + const container = document.getElementById('echTooltipContainerPortal'); + if (container) { + this.portalNode = container as HTMLDivElement; + } else { + this.portalNode = document.createElement('div'); + this.portalNode.id = 'echTooltipContainerPortal'; + document.body.appendChild(this.portalNode); + } + } + componentDidMount() { + this.createPortalNode(); + } + + componentDidUpdate() { + this.createPortalNode(); + const { getChartContainerRef } = this.props; + const { tooltipPosition } = this.props.chartStore!; + const chartContainerRef = getChartContainerRef(); + + if (!this.tooltipRef.current || !chartContainerRef.current || !this.portalNode) { + return; + } + + const chartContainerBBox = chartContainerRef.current.getBoundingClientRect(); + const tooltipBBox = this.tooltipRef.current.getBoundingClientRect(); + const tooltipStyle = getFinalTooltipPosition(chartContainerBBox, tooltipBBox, tooltipPosition); + + this.portalNode.style.left = tooltipStyle.left; + this.portalNode.style.top = tooltipStyle.top; + } + + componentWillUnmount() { + if (this.portalNode && this.portalNode.parentNode) { + this.portalNode.parentNode.removeChild(this.portalNode); + } + } renderHeader(headerData?: TooltipValue, formatter?: TooltipValueFormatter) { if (!headerData) { @@ -20,37 +67,44 @@ class TooltipsComponent extends React.Component { } render() { - const { isTooltipVisible, tooltipData, tooltipPosition, tooltipHeaderFormatter } = this.props.chartStore!; - - if (!isTooltipVisible.get()) { - return
; + const { isTooltipVisible, tooltipData, tooltipHeaderFormatter } = this.props.chartStore!; + const isVisible = isTooltipVisible.get(); + let tooltip; + if (!this.portalNode) { + return null; } - - return ( -
-
{this.renderHeader(tooltipData[0], tooltipHeaderFormatter)}
-
- {tooltipData.slice(1).map(({ name, value, color, isHighlighted, seriesKey, yAccessor }) => { - const classes = classNames('echTooltip__item', { - /* eslint @typescript-eslint/camelcase:0 */ - echTooltip__rowHighlighted: isHighlighted, - }); - return ( -
- {name} - {value} -
- ); - })} + const { getChartContainerRef } = this.props; + const chartContainerRef = getChartContainerRef(); + if (chartContainerRef.current === null || !isVisible) { + return null; + } else { + tooltip = ( +
+
{this.renderHeader(tooltipData[0], tooltipHeaderFormatter)}
+
+ {tooltipData.slice(1).map(({ name, value, color, isHighlighted, seriesKey, yAccessor }) => { + const classes = classNames('echTooltip__item', { + /* eslint @typescript-eslint/camelcase:0 */ + echTooltip__rowHighlighted: isHighlighted, + }); + return ( +
+ {name} + {value} +
+ ); + })} +
-
- ); + ); + } + return createPortal(tooltip, this.portalNode); } } diff --git a/src/utils/data_generators/data_generator.ts b/src/utils/data_generators/data_generator.ts index b7fc3ff0f9..a58c841303 100644 --- a/src/utils/data_generators/data_generator.ts +++ b/src/utils/data_generators/data_generator.ts @@ -7,19 +7,19 @@ export class DataGenerator { this.generator = new Simple1DNoise(randomNumberGenerator); this.frequency = frequency; } - generateSimpleSeries(totalPoints = 50, group = 1) { + generateSimpleSeries(totalPoints = 50, group = 1, groupPrefix = '') { const dataPoints = new Array(totalPoints).fill(0).map((_, i) => { return { x: i, y: 3 + Math.sin(i / this.frequency) + this.generator.getValue(i), - g: group, + g: `${groupPrefix}${group}`, }; }); return dataPoints; } - generateGroupedSeries(totalPoints = 50, totalGroups = 2) { + generateGroupedSeries(totalPoints = 50, totalGroups = 2, groupPrefix = '') { const groups = new Array(totalGroups).fill(0).map((group, i) => { - return this.generateSimpleSeries(totalPoints, i); + return this.generateSimpleSeries(totalPoints, i, groupPrefix); }); return groups.reduce((acc, curr) => [...acc, ...curr]); } diff --git a/stories/bar_chart.tsx b/stories/bar_chart.tsx index 7e50bea811..a4536105f7 100644 --- a/stories/bar_chart.tsx +++ b/stories/bar_chart.tsx @@ -1701,4 +1701,28 @@ storiesOf('Bar Chart', module) /> ); + }) + .add('[test] tooltip and rotation', () => { + return ( + + + + Number(d).toFixed(2)} + /> + + + + ); });