diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-bottom-legend-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-bottom-legend-1-snap.png new file mode 100644 index 0000000000..774f10d22a Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-bottom-legend-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-left-legend-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-left-legend-1-snap.png new file mode 100644 index 0000000000..bdb10b7b7e Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-left-legend-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-right-legend-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-right-legend-1-snap.png new file mode 100644 index 0000000000..e41e650e30 Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-right-legend-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-top-legend-1-snap.png b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-top-legend-1-snap.png new file mode 100644 index 0000000000..b61696a30a Binary files /dev/null and b/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-tooltip-placement-with-legend-should-render-tooltip-with-top-legend-1-snap.png differ diff --git a/integration/tests/legend_stories.test.ts b/integration/tests/legend_stories.test.ts index 5a1d684c98..2b827cfed7 100644 --- a/integration/tests/legend_stories.test.ts +++ b/integration/tests/legend_stories.test.ts @@ -62,4 +62,34 @@ describe('Legend stories', () => { delay: 200, // needed for icon to load }); }); + + describe('Tooltip placement with legend', () => { + it('should render tooltip with left legend', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot('http://localhost:9001/?path=/story/legend--left', { + bottom: 190, + left: 310, + }); + }); + + it('should render tooltip with top legend', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot('http://localhost:9001/?path=/story/legend--top', { + top: 150, + left: 320, + }); + }); + + it('should render tooltip with right legend', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot('http://localhost:9001/?path=/story/legend--right', { + bottom: 180, + left: 330, + }); + }); + + it('should render tooltip with bottom legend', async () => { + await common.expectChartWithMouseAtUrlToMatchScreenshot('http://localhost:9001/?path=/story/legend--bottom', { + top: 150, + left: 320, + }); + }); + }); }); diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts index 1b4954b060..4e5651b351 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.ts @@ -23,6 +23,7 @@ import { Rotation } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { Point } from '../../../utils/point'; import { isHorizontalRotation, isVerticalRotation } from '../state/utils/common'; +import { ChartDimensions } from '../utils/dimensions'; export interface SnappedPosition { position: number; @@ -168,26 +169,25 @@ export function getCursorBandPosition( /** @internal */ export function getTooltipAnchorPosition( - chartDimensions: Dimensions, + { chartDimensions, offset }: ChartDimensions, chartRotation: Rotation, cursorBandPosition: Dimensions, cursorPosition: { x: number; y: number }, - isSingleValueXScale: boolean, ): TooltipAnchorPosition { const isRotated = isVerticalRotation(chartRotation); const hPosition = getHorizontalTooltipPosition( cursorPosition.x, cursorBandPosition, chartDimensions, + offset.left, isRotated, - isSingleValueXScale, ); const vPosition = getVerticalTooltipPosition( cursorPosition.y, cursorBandPosition, chartDimensions, + offset.top, isRotated, - isSingleValueXScale, ); return { isRotated, @@ -200,19 +200,19 @@ function getHorizontalTooltipPosition( cursorXPosition: number, cursorBandPosition: Dimensions, chartDimensions: Dimensions, + globalOffset: number, isRotated: boolean, - isSingleValueXScale: boolean, ): { x0?: number; x1: number } { if (!isRotated) { return { - x0: cursorBandPosition.left, - x1: cursorBandPosition.left + (isSingleValueXScale ? 0 : cursorBandPosition.width), + x0: cursorBandPosition.left + globalOffset, + x1: cursorBandPosition.left + cursorBandPosition.width + globalOffset, }; } return { // NOTE: x0 set to zero blocks tooltip placement on left when rotated 90 deg // Delete this comment before merging and verifing this doesn't break anything. - x1: chartDimensions.left + cursorXPosition, + x1: chartDimensions.left + cursorXPosition + globalOffset, }; } @@ -220,20 +220,21 @@ function getVerticalTooltipPosition( cursorYPosition: number, cursorBandPosition: Dimensions, chartDimensions: Dimensions, + globalOffset: number, isRotated: boolean, - isSingleValueXScale: boolean, ): { y0: number; y1: number; } { if (!isRotated) { + const y = cursorYPosition + chartDimensions.top + globalOffset; return { - y0: cursorYPosition + chartDimensions.top, - y1: cursorYPosition + chartDimensions.top, + y0: y, + y1: y, }; } return { - y0: cursorBandPosition.top, - y1: (isSingleValueXScale ? 0 : cursorBandPosition.height) + cursorBandPosition.top, + y0: cursorBandPosition.top + globalOffset, + y1: cursorBandPosition.height + cursorBandPosition.top + globalOffset, }; } diff --git a/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts b/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts index 9d0908db56..5bd5da46fa 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_chart_dimensions.ts @@ -22,8 +22,9 @@ import createCachedSelector from 're-reselect'; import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; -import { Dimensions } from '../../../../utils/dimensions'; -import { computeChartDimensions } from '../../utils/dimensions'; +import { getLegendSizeSelector, LegendSizing } from '../../../../state/selectors/get_legend_size'; +import { Position } from '../../../../utils/commons'; +import { computeChartDimensions, ChartDimensions } from '../../utils/dimensions'; import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensions'; import { getAxesStylesSelector } from './get_axis_styles'; import { getAxisSpecsSelector } from './get_specs'; @@ -36,15 +37,39 @@ export const computeChartDimensionsSelector = createCachedSelector( computeAxisTicksDimensionsSelector, getAxisSpecsSelector, getAxesStylesSelector, + getLegendSizeSelector, ], - ( - chartContainerDimensions, - chartTheme, - axesTicksDimensions, - axesSpecs, - axesStyles, - ): { - chartDimensions: Dimensions; - leftMargin: number; - } => computeChartDimensions(chartContainerDimensions, chartTheme, axesTicksDimensions, axesStyles, axesSpecs), + (chartContainerDimensions, chartTheme, axesTicksDimensions, axesSpecs, axesStyles, legendSize): ChartDimensions => + computeChartDimensions( + chartContainerDimensions, + chartTheme, + axesTicksDimensions, + axesStyles, + axesSpecs, + getLegendDimension(legendSize), + ), )(getChartIdSelector); + +function getLegendDimension({ + position, + width, + height, + margin, +}: LegendSizing): { + top: number; + left: number; +} { + let left = 0; + let top = 0; + + if (position === Position.Left) { + left = width + margin * 2; + } else if (position === Position.Top) { + top = height + margin * 2; + } + + return { + left, + top, + }; +} diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts index 951a2ed782..bc58216492 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_position.ts @@ -21,10 +21,10 @@ import createCachedSelector from 're-reselect'; import { TooltipAnchorPosition } from '../../../../components/tooltip/types'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getLegendSizeSelector } from '../../../../state/selectors/get_legend_size'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { getTooltipAnchorPosition } from '../../crosshair/crosshair_utils'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; -import { getComputedScalesSelector } from './get_computed_scales'; import { getCursorBandPositionSelector } from './get_cursor_band'; import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; @@ -35,24 +35,13 @@ export const getTooltipAnchorPositionSelector = createCachedSelector( getSettingsSpecSelector, getCursorBandPositionSelector, getProjectedPointerPositionSelector, - getComputedScalesSelector, + getLegendSizeSelector, ], - ( - { chartDimensions }, - settings, - cursorBandPosition, - projectedPointerPosition, - scales, - ): TooltipAnchorPosition | null => { + (chartDimensions, settings, cursorBandPosition, projectedPointerPosition): TooltipAnchorPosition | null => { if (!cursorBandPosition) { return null; } - return getTooltipAnchorPosition( - chartDimensions, - settings.rotation, - cursorBandPosition, - projectedPointerPosition, - scales.xScale.isSingleValue(), - ); + + return getTooltipAnchorPosition(chartDimensions, settings.rotation, cursorBandPosition, projectedPointerPosition); }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/utils/dimensions.test.ts b/src/chart_types/xy_chart/utils/dimensions.test.ts index 42ca8cdea5..e6c795395e 100644 --- a/src/chart_types/xy_chart/utils/dimensions.test.ts +++ b/src/chart_types/xy_chart/utils/dimensions.test.ts @@ -47,6 +47,10 @@ describe('Computed chart dimensions', () => { top: 10, bottom: 10, }; + const legendSize = { + top: 0, + left: 0, + }; const axis1Dims: AxisTicksDimensions = { tickValues: [0, 1], @@ -89,7 +93,14 @@ describe('Computed chart dimensions', () => { const axisDims = new Map(); const axisStyles = new Map(); const axisSpecs: AxisSpec[] = []; - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const { chartDimensions } = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisStyles, + axisSpecs, + legendSize, + ); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -101,7 +112,14 @@ describe('Computed chart dimensions', () => { const axisStyles = new Map(); const axisSpecs = [axisLeftSpec]; axisDims.set('axis_1', axis1Dims); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const { chartDimensions } = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisStyles, + axisSpecs, + legendSize, + ); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -113,7 +131,14 @@ describe('Computed chart dimensions', () => { const axisStyles = new Map(); const axisSpecs = [{ ...axisLeftSpec, position: Position.Right }]; axisDims.set('axis_1', axis1Dims); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const { chartDimensions } = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisStyles, + axisSpecs, + legendSize, + ); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -130,7 +155,14 @@ describe('Computed chart dimensions', () => { }, ]; axisDims.set('axis_1', axis1Dims); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const { chartDimensions } = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisStyles, + axisSpecs, + legendSize, + ); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -147,7 +179,14 @@ describe('Computed chart dimensions', () => { }, ]; axisDims.set('axis_1', axis1Dims); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const { chartDimensions } = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisStyles, + axisSpecs, + legendSize, + ); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -162,7 +201,7 @@ describe('Computed chart dimensions', () => { }, ]; axisDims.set('foo', axis1Dims); - const chartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const chartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs, legendSize); const expectedDims = { chartDimensions: { @@ -172,6 +211,10 @@ describe('Computed chart dimensions', () => { top: 20, }, leftMargin: 10, + offset: { + top: 0, + left: 0, + }, }; expect(chartDimensions).toEqual(expectedDims); @@ -184,7 +227,14 @@ describe('Computed chart dimensions', () => { hide: true, position: Position.Bottom, }); - const hiddenAxisChartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisStyles, axisSpecs); + const hiddenAxisChartDimensions = computeChartDimensions( + parentDim, + chartTheme, + axisDims, + axisStyles, + axisSpecs, + legendSize, + ); expect(hiddenAxisChartDimensions).toEqual(expectedDims); }); diff --git a/src/chart_types/xy_chart/utils/dimensions.ts b/src/chart_types/xy_chart/utils/dimensions.ts index 9bc152d0f7..7d4642d3b2 100644 --- a/src/chart_types/xy_chart/utils/dimensions.ts +++ b/src/chart_types/xy_chart/utils/dimensions.ts @@ -25,6 +25,27 @@ import { getSpecsById } from '../state/utils/spec'; import { AxisTicksDimensions, shouldShowTicks } from './axis_utils'; import { AxisSpec } from './specs'; +/** + * @internal + */ +export interface ChartDimensions { + /** + * Dimensions relative to canvas element + */ + chartDimensions: Dimensions; + /** + * Dimensions relative to echChart element + */ + offset: { + top: number; + left: number; + }; + /** + * Margin to account for ending text overflow + */ + leftMargin: number; +} + /** * Compute the chart dimensions. It's computed removing from the parent dimensions * the axis spaces, the legend and any other specified style margin and padding. @@ -40,10 +61,11 @@ export function computeChartDimensions( axisDimensions: Map, axesStyles: Map, axisSpecs: AxisSpec[], -): { - chartDimensions: Dimensions; - leftMargin: number; -} { + legendSizing: { + top: number; + left: number; + }, +): ChartDimensions { if (parentDimensions.width <= 0 || parentDimensions.height <= 0) { return { chartDimensions: { @@ -53,6 +75,10 @@ export function computeChartDimensions( top: 0, }, leftMargin: 0, + offset: { + left: 0, + top: 0, + }, }; } @@ -120,5 +146,9 @@ export function computeChartDimensions( width: chartWidth - chartPaddings.left - chartPaddings.right, height: chartHeight - chartPaddings.top - chartPaddings.bottom, }, + offset: { + top: legendSizing.top, + left: legendSizing.left, + }, }; } diff --git a/src/state/selectors/get_legend_size.ts b/src/state/selectors/get_legend_size.ts index 90f3dad277..5ab5deb09e 100644 --- a/src/state/selectors/get_legend_size.ts +++ b/src/state/selectors/get_legend_size.ts @@ -23,7 +23,7 @@ import { isVerticalAxis } from '../../chart_types/xy_chart/utils/axis_type_utils import { LEGEND_HIERARCHY_MARGIN } from '../../components/legend/legend_item'; import { BBox } from '../../utils/bbox/bbox_calculator'; import { CanvasTextBBoxCalculator } from '../../utils/bbox/canvas_text_bbox_calculator'; -import { isDefined } from '../../utils/commons'; +import { Position, isDefined } from '../../utils/commons'; import { GlobalChartState } from '../chart_state'; import { getChartIdSelector } from './get_chart_id'; import { getChartThemeSelector } from './get_chart_theme'; @@ -37,10 +37,16 @@ const MARKER_LEFT_MARGIN = 4; const VALUE_LEFT_MARGIN = 4; const VERTICAL_PADDING = 4; +/** @internal */ +export type LegendSizing = BBox & { + margin: number; + position: Position; +}; + /** @internal */ export const getLegendSizeSelector = createCachedSelector( [getSettingsSpecSelector, getChartThemeSelector, getParentDimensionSelector, getLegendItemsLabelsSelector], - (settings, theme, parentDimensions, labels): BBox & { margin: number } => { + (settings, theme, parentDimensions, labels): LegendSizing => { const bboxCalculator = new CanvasTextBBoxCalculator(); const bbox = labels.reduce( (acc, { label, depth }) => { @@ -65,22 +71,23 @@ export const getLegendSizeSelector = createCachedSelector( ); bboxCalculator.destroy(); - const { showLegend, showLegendExtra: showLegendDisplayValue, legendPosition, legendAction } = settings; + const { showLegend, showLegendExtra: showLegendDisplayValue, legendPosition: position, legendAction } = settings; const { legend: { verticalWidth, spacingBuffer, margin }, } = theme; if (!showLegend) { - return { width: 0, height: 0, margin: 0 }; + return { width: 0, height: 0, margin: 0, position }; } const actionDimension = isDefined(legendAction) ? 24 : 0; // max width plus margin const legendItemWidth = MARKER_WIDTH + MARKER_LEFT_MARGIN + bbox.width + (showLegendDisplayValue ? VALUE_LEFT_MARGIN : 0); - if (isVerticalAxis(legendPosition)) { + if (isVerticalAxis(position)) { const legendItemHeight = bbox.height + VERTICAL_PADDING * 2; return { width: Math.floor(Math.min(legendItemWidth + spacingBuffer + actionDimension, verticalWidth)), height: legendItemHeight, margin, + position, }; } const isSingleLine = (parentDimensions.width - 20) / 200 > labels.length; @@ -88,6 +95,7 @@ export const getLegendSizeSelector = createCachedSelector( height: isSingleLine ? bbox.height + 16 : bbox.height * 2 + 24, width: Math.floor(Math.min(legendItemWidth + spacingBuffer + actionDimension, verticalWidth)), margin, + position, }; }, )(getChartIdSelector);