diff --git a/.playground/index.html b/.playground/index.html index 3973665311..fe305c8bcd 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -23,15 +23,23 @@ */ /* width: 100%; height: 100%;*/ + /* overflow-x: hidden; */ } .chart { background: white; /*display: inline-block; position: relative; */ - width: 300px; - height: 300px; - margin: 20px; + width: 100%; + height: 500px; + overflow: auto; + } + + .testing { + background: aquamarine; + position: relative; + width: 100vw; + overflow: auto; } diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 3a170dc677..df9ed05a59 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -17,7 +17,8 @@ * under the License. */ import React from 'react'; -import { Chart, Partition, Settings, PartitionLayout, XYChartElementEvent, PartitionElementEvent } from '../src'; +import { XYChartElementEvent, PartitionElementEvent } from '../src'; +import { example } from '../stories/treemap/6_custom_style'; export class Playground extends React.Component { onElementClick = (elements: (XYChartElementEvent | PartitionElementEvent)[]) => { @@ -26,33 +27,8 @@ export class Playground extends React.Component { }; render() { return ( -
- - - { - return d.v; - }} - data={[ - { g1: 'a', g2: 'a', v: 1 }, - { g1: 'a', g2: 'b', v: 1 }, - { g1: 'b', g2: 'a', v: 1 }, - { g1: 'b', g2: 'b', v: 1 }, - ]} - layers={[ - { - groupByRollup: (datum: { g1: string }) => datum.g1, - }, - { - groupByRollup: (datum: { g2: string }) => datum.g2, - }, - ]} - /> - +
+
{example()}
); } diff --git a/src/chart_types/xy_chart/annotations/annotation_tooltip.ts b/src/chart_types/xy_chart/annotations/annotation_tooltip.ts index 1df2745586..7ee3f9946f 100644 --- a/src/chart_types/xy_chart/annotations/annotation_tooltip.ts +++ b/src/chart_types/xy_chart/annotations/annotation_tooltip.ts @@ -17,6 +17,7 @@ * under the License. */ import { Dimensions } from '../../../utils/dimensions'; +import { Position } from '../../../utils/commons'; /** @internal */ export function getFinalAnnotationTooltipPosition( @@ -27,16 +28,21 @@ export function getFinalAnnotationTooltipPosition( tooltip: Dimensions, /** the tooltip computed position not adjusted within chart bounds */ tooltipAnchor: { top: number; left: number }, + /** the width of the tooltip portal container */ + portalWidth: number, padding = 10, ): { left: string | null; top: string | null; + anchor: 'left' | 'right'; } { let left = 0; + let anchor: Position = Position.Left; 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; + if (chartDimensions.left + tooltipAnchor.left + portalWidth + padding >= container.width) { + left = annotationXOffset - portalWidth - padding; + anchor = Position.Right; } else { left = annotationXOffset + padding; } @@ -50,5 +56,6 @@ export function getFinalAnnotationTooltipPosition( return { left: `${Math.round(left)}px`, top: `${Math.round(top)}px`, + anchor, }; } diff --git a/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts b/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts index badc0eb8ff..bf171c4845 100644 --- a/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts +++ b/src/chart_types/xy_chart/crosshair/crosshair_utils.test.ts @@ -31,9 +31,10 @@ describe('Tooltip position', () => { top: 0, left: 0, }; + const portalWidth = 50; describe('horizontal rotated chart', () => { it('can position the tooltip on the top left corner', () => { - const position = getFinalTooltipPosition(container, tooltip, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: false, y1: 0, y0: 0, @@ -44,8 +45,9 @@ describe('Tooltip position', () => { 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, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: false, y0: 90, y1: 90, @@ -56,8 +58,9 @@ describe('Tooltip position', () => { 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, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: false, y0: 0, y1: 0, @@ -65,11 +68,12 @@ describe('Tooltip position', () => { x1: 100, padding: 5, }); - expect(position.left).toBe('65px'); + expect(position.left).toBe('55px'); expect(position.top).toBe('10px'); }); + it('can position the tooltip on the bottom right corner', () => { - const position = getFinalTooltipPosition(container, tooltip, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: false, y0: 90, y1: 90, @@ -77,13 +81,38 @@ describe('Tooltip position', () => { x1: 100, padding: 5, }); - expect(position.left).toBe('65px'); + expect(position.left).toBe('55px'); expect(position.top).toBe('80px'); }); + + it('should render on right if portal width is within right side', () => { + const position = getFinalTooltipPosition(container, tooltip, 44, { + isRotated: false, + y0: 0, + y1: 0, + x0: 50, + x1: 50, + padding: 5, + }); + expect(position.left).toBe('65px'); + }); + + it('should render on left if portal width is NOT within right side', () => { + const position = getFinalTooltipPosition(container, tooltip, 46, { + isRotated: false, + y0: 0, + y1: 0, + x0: 50, + x1: 50, + padding: 5, + }); + expect(position.left).toBe('9px'); + }); }); + describe('vertical rotated chart', () => { it('can position the tooltip on the top left corner', () => { - const position = getFinalTooltipPosition(container, tooltip, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: true, y0: 0, y1: 0, @@ -94,8 +123,9 @@ describe('Tooltip position', () => { 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, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: true, y0: 90, y1: 90, @@ -106,8 +136,9 @@ describe('Tooltip position', () => { 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, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: true, y0: 0, y1: 0, @@ -118,8 +149,9 @@ describe('Tooltip position', () => { 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, { + const position = getFinalTooltipPosition(container, tooltip, portalWidth, { isRotated: true, y0: 90, y1: 90, @@ -130,5 +162,29 @@ describe('Tooltip position', () => { expect(position.left).toBe('70px'); expect(position.top).toBe('65px'); }); + + it('should render on right if portal width is within right side', () => { + const position = getFinalTooltipPosition(container, tooltip, 44, { + isRotated: true, + y0: 0, + y1: 0, + x0: 50, + x1: 50, + padding: 5, + }); + expect(position.left).toBe('60px'); + }); + + it('should render on left if portal width is NOT within right side', () => { + const position = getFinalTooltipPosition(container, tooltip, 51, { + isRotated: true, + y0: 0, + y1: 0, + x0: 50, + x1: 50, + padding: 5, + }); + expect(position.left).toBe('70px'); + }); }); }); diff --git a/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx b/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx index 921efaf451..b88ff7cf39 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotation_tooltips.tsx @@ -58,6 +58,12 @@ class AnnotationTooltipComponent extends React.Component static displayName = 'AnnotationTooltip'; portalNode: HTMLDivElement | null = null; tooltipRef: React.RefObject; + /** + * Max allowable width for tooltip to grow to. Used to determine container fit. + * + * @unit px + */ + static MAX_WIDTH = 256; constructor(props: AnnotationTooltipProps) { super(props); @@ -71,6 +77,7 @@ class AnnotationTooltipComponent extends React.Component } else { this.portalNode = document.createElement('div'); this.portalNode.id = ANNOTATION_CONTAINER_ID; + this.portalNode.style.width = `${AnnotationTooltipComponent.MAX_WIDTH}px`; document.body.appendChild(this.portalNode); } } @@ -96,12 +103,15 @@ class AnnotationTooltipComponent extends React.Component } const chartContainerBBox = chartContainerRef.current.getBoundingClientRect(); + const width = Math.min(AnnotationTooltipComponent.MAX_WIDTH, chartContainerBBox.width * 0.7); + this.portalNode.style.width = `${width}px`; const tooltipBBox = this.tooltipRef.current.getBoundingClientRect(); const tooltipStyle = getFinalAnnotationTooltipPosition( chartContainerBBox, chartDimensions, tooltipBBox, tooltipState.anchor, + width, ); if (tooltipStyle.left) { @@ -110,6 +120,17 @@ class AnnotationTooltipComponent extends React.Component if (tooltipStyle.top) { this.portalNode.style.top = tooltipStyle.top; } + + if (tooltipStyle.left) { + this.portalNode.style.left = tooltipStyle.left; + if (this.tooltipRef.current) { + this.tooltipRef.current.style.left = tooltipStyle.anchor === 'right' ? 'auto' : '0px'; + this.tooltipRef.current.style.right = tooltipStyle.anchor === 'right' ? '0px' : 'auto'; + } + } + if (tooltipStyle.top) { + this.portalNode.style.top = tooltipStyle.top; + } } componentWillUnmount() { diff --git a/src/components/_annotation.scss b/src/components/_annotation.scss index ccebc89a7e..f7a5a63cde 100644 --- a/src/components/_annotation.scss +++ b/src/components/_annotation.scss @@ -1,6 +1,6 @@ #echAnnotationContainerPortal { position: absolute; - width: 256px; + pointer-events: none; } .echAnnotation { pointer-events: none; diff --git a/src/components/tooltip/_tooltip.scss b/src/components/tooltip/_tooltip.scss index 8999ca7317..3949d05e38 100644 --- a/src/components/tooltip/_tooltip.scss +++ b/src/components/tooltip/_tooltip.scss @@ -1,6 +1,7 @@ #echTooltipContainerPortal { position: absolute; - width: 256px; + pointer-events: none; + z-index: 10000000; } .echTooltip { position: absolute; @@ -41,7 +42,7 @@ font-weight: $euiFontWeightBold; text-align: right; font-feature-settings: 'tnum'; - margin-left: 8px; + margin-left: $euiSizeS; } &__rowHighlighted { diff --git a/src/components/tooltip/index.ts b/src/components/tooltip/index.ts new file mode 100644 index 0000000000..695a135bc8 --- /dev/null +++ b/src/components/tooltip/index.ts @@ -0,0 +1,20 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +/** @internal */ +export { TooltipPortal as Tooltip } from './tooltip_portal'; diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx new file mode 100644 index 0000000000..6197beb801 --- /dev/null +++ b/src/components/tooltip/tooltip.tsx @@ -0,0 +1,75 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import classNames from 'classnames'; +import React, { forwardRef, memo, useCallback } from 'react'; +import { TooltipInfo } from './types'; +import { TooltipValueFormatter, TooltipValue } from '../../specs'; + +interface TooltipProps { + info: TooltipInfo; + headerFormatter?: TooltipValueFormatter; +} + +const TooltipComponent = forwardRef(({ info, headerFormatter }, ref) => { + const renderHeader = useCallback( + (headerData: TooltipValue | null, formatter?: TooltipValueFormatter) => { + if (!headerData || !headerData.isVisible) { + return null; + } + return
{formatter ? formatter(headerData) : headerData.value}
; + }, + [info.header, headerFormatter], + ); + + return ( +
+ {renderHeader(info.header, headerFormatter)} +
+ {info.values.map( + ({ seriesIdentifier, valueAccessor, label, value, markValue, color, isHighlighted, isVisible }, index) => { + if (!isVisible) { + return null; + } + const classes = classNames('echTooltip__item', { + /* eslint @typescript-eslint/camelcase:0 */ + echTooltip__rowHighlighted: isHighlighted, + }); + return ( +
+ {label} + {value} + {markValue &&  ({markValue})} +
+ ); + }, + )} +
+
+ ); +}); + +/** @internal */ +export const Tooltip = memo(TooltipComponent); diff --git a/src/components/tooltip/index.tsx b/src/components/tooltip/tooltip_portal.tsx similarity index 64% rename from src/components/tooltip/index.tsx rename to src/components/tooltip/tooltip_portal.tsx index dcf37459f0..8033637d50 100644 --- a/src/components/tooltip/index.tsx +++ b/src/components/tooltip/tooltip_portal.tsx @@ -16,38 +16,44 @@ * specific language governing permissions and limitations * under the License. */ -import classNames from 'classnames'; import React from 'react'; import { createPortal } from 'react-dom'; import { connect } from 'react-redux'; import { getFinalTooltipPosition, TooltipAnchorPosition } from './utils'; import { TooltipInfo } from './types'; -import { TooltipValueFormatter, TooltipValue } from '../../specs'; +import { TooltipValueFormatter } from '../../specs'; import { GlobalChartState, BackwardRef } from '../../state/chart_state'; import { isInitialized } from '../../state/selectors/is_initialized'; import { getInternalIsTooltipVisibleSelector } from '../../state/selectors/get_internal_is_tooltip_visible'; import { getTooltipHeaderFormatterSelector } from '../../state/selectors/get_tooltip_header_formatter'; import { getInternalTooltipInfoSelector } from '../../state/selectors/get_internal_tooltip_info'; import { getInternalTooltipAnchorPositionSelector } from '../../state/selectors/get_internal_tooltip_anchor_position'; +import { Tooltip } from './tooltip'; -interface TooltipStateProps { +interface TooltipPortalStateProps { isVisible: boolean; position: TooltipAnchorPosition | null; info?: TooltipInfo; headerFormatter?: TooltipValueFormatter; } -interface TooltipOwnProps { +interface TooltipPortalOwnProps { getChartContainerRef: BackwardRef; } -type TooltipProps = TooltipStateProps & TooltipOwnProps; +type TooltipPortalProps = TooltipPortalStateProps & TooltipPortalOwnProps; -class TooltipComponent extends React.Component { +class TooltipPortalComponent extends React.Component { static displayName = 'Tooltip'; + /** + * Max allowable width for tooltip to grow to. Used to determine container fit. + * + * @unit px + */ + static MAX_WIDTH = 256; portalNode: HTMLDivElement | null = null; tooltipRef: React.RefObject; - constructor(props: TooltipProps) { + constructor(props: TooltipPortalProps) { super(props); this.tooltipRef = React.createRef(); } @@ -58,6 +64,7 @@ class TooltipComponent extends React.Component { } else { this.portalNode = document.createElement('div'); this.portalNode.id = 'echTooltipContainerPortal'; + this.portalNode.style.width = `${TooltipPortalComponent.MAX_WIDTH}px`; document.body.appendChild(this.portalNode); } } @@ -76,10 +83,16 @@ class TooltipComponent extends React.Component { const chartContainerBBox = chartContainerRef.current.getBoundingClientRect(); const tooltipBBox = this.tooltipRef.current.getBoundingClientRect(); - const tooltipStyle = getFinalTooltipPosition(chartContainerBBox, tooltipBBox, position); + const width = Math.min(TooltipPortalComponent.MAX_WIDTH, chartContainerBBox.width * 0.7); + this.portalNode.style.width = `${width}px`; + const tooltipStyle = getFinalTooltipPosition(chartContainerBBox, tooltipBBox, width, position); if (tooltipStyle.left) { this.portalNode.style.left = tooltipStyle.left; + if (this.tooltipRef.current) { + this.tooltipRef.current.style.left = tooltipStyle.anchor === 'right' ? 'auto' : '0px'; + this.tooltipRef.current.style.right = tooltipStyle.anchor === 'right' ? '0px' : 'auto'; + } } if (tooltipStyle.top) { this.portalNode.style.top = tooltipStyle.top; @@ -92,50 +105,6 @@ class TooltipComponent extends React.Component { } } - renderHeader(headerData: TooltipValue | null, formatter?: TooltipValueFormatter) { - if (!headerData || !headerData.isVisible) { - return null; - } - return
{formatter ? formatter(headerData) : headerData.value}
; - } - - renderTooltip = (info: TooltipInfo) => { - const { headerFormatter } = this.props; - - return ( -
- {this.renderHeader(info.header, headerFormatter)} -
- {info.values.map( - ({ seriesIdentifier, valueAccessor, label, value, markValue, color, isHighlighted, isVisible }, index) => { - if (!isVisible) { - return null; - } - const classes = classNames('echTooltip__item', { - /* eslint @typescript-eslint/camelcase:0 */ - echTooltip__rowHighlighted: isHighlighted, - }); - return ( -
- {label} - {value} - {markValue &&  ({markValue})} -
- ); - }, - )} -
-
- ); - }; - render() { const { isVisible, info, getChartContainerRef } = this.props; const chartContainerRef = getChartContainerRef(); @@ -144,7 +113,10 @@ class TooltipComponent extends React.Component { return null; } - return createPortal(this.renderTooltip(info), this.portalNode); + return createPortal( + , + this.portalNode, + ); } } @@ -155,7 +127,7 @@ const HIDDEN_TOOLTIP_PROPS = { headerFormatter: undefined, }; -const mapStateToProps = (state: GlobalChartState): TooltipStateProps => { +const mapStateToProps = (state: GlobalChartState): TooltipPortalStateProps => { if (!isInitialized(state)) { return HIDDEN_TOOLTIP_PROPS; } @@ -168,4 +140,4 @@ const mapStateToProps = (state: GlobalChartState): TooltipStateProps => { }; /** @internal */ -export const Tooltip = connect(mapStateToProps)(TooltipComponent); +export const TooltipPortal = connect(mapStateToProps)(TooltipPortalComponent); diff --git a/src/components/tooltip/utils.ts b/src/components/tooltip/utils.ts index 0810254635..11e9682106 100644 --- a/src/components/tooltip/utils.ts +++ b/src/components/tooltip/utils.ts @@ -52,11 +52,14 @@ export function getFinalTooltipPosition( container: Dimensions, /** the dimensions of the tooltip container */ tooltip: Dimensions, + /** the width of the tooltip portal container */ + portalWidth: number, /** the tooltip anchor computed position not adjusted within chart bounds */ anchorPosition: TooltipAnchorPosition, ): { left: string | null; top: string | null; + anchor: 'left' | 'right'; } { const { x1, y1, isRotated, padding = 10 } = anchorPosition; let left = 0; @@ -64,11 +67,13 @@ export function getFinalTooltipPosition( const x0 = anchorPosition.x0 || anchorPosition.x1; const y0 = anchorPosition.y0 || anchorPosition.y1; + let anchor: 'left' | 'right' = 'left' as 'left'; if (!isRotated) { const leftOfBand = window.pageXOffset + container.left + x0; - if (x1 + tooltip.width + padding > container.width) { - left = leftOfBand - tooltip.width - padding; + if (x1 + portalWidth + padding > container.width) { + left = leftOfBand - portalWidth - padding; + anchor = 'right' as 'right'; } else { left = leftOfBand + (x1 - x0) + padding; } @@ -79,8 +84,9 @@ export function getFinalTooltipPosition( top = topOfBand + y0; } } else { + // not sure if this is also fixed no rotated charts const leftOfBand = window.pageXOffset + container.left; - if (x1 + tooltip.width > container.width) { + if (x1 + portalWidth > container.width) { left = leftOfBand + container.width - tooltip.width; } else { left = leftOfBand + x1; @@ -96,5 +102,6 @@ export function getFinalTooltipPosition( return { left: `${Math.round(left)}px`, top: `${Math.round(top)}px`, + anchor, }; }