diff --git a/.playground/index.html b/.playground/index.html index 4f0a564770..2403a54bfc 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -9,24 +9,6 @@ html, body { background: blanchedalmond !important; - /*margin-left: 8px !important;*/ - /*padding: 8px !important;*/ - /*height: 100%;*/ - /*width: 2000px; - } - #root { - position: absolute; - /* - top: 0; - left: 0; -*/ - /* width: 100%; - height: 100%;*/ - /* overflow-x: hidden; */ - } - - #root { - height: 500px; } .chart { @@ -34,8 +16,8 @@ /*display: inline-block; position: relative; */ - width: 100%; - height: 500px; + width: 500px; + height: 200px; overflow: auto; } @@ -47,7 +29,7 @@ } .page { - padding: 100px; + padding: 10px; } label { diff --git a/.playground/playground.tsx b/.playground/playground.tsx index e19342b187..40ad254eee 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -17,78 +17,210 @@ * under the License. */ -import React, { useState } from 'react'; - -import { Chart, BarSeries, LegendColorPicker, Settings, ScaleType } from '../src'; -import { SeededDataGenerator } from '../src/mocks/utils'; - -const dg = new SeededDataGenerator(); - -type SetColorFn = (color: string) => void; -const legendColorPickerFn = (setColors: SetColorFn, customColor: string): LegendColorPicker => ({ onClose }) => ( -
- Custom Color Picker - - +import React from 'react'; + +import { + Chart, + Settings, + Axis, + Position, + BarSeries, + ScaleType, + PointerEvent, + LineSeries, + CustomTooltip, + TooltipType, + LineAnnotation, AnnotationDomainTypes, +} from '../src'; +import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; + + +const TestCustomTooltip: CustomTooltip = (props) => ( +
+

Testing a custom tooltip

+
); -function LegendColorPickerMock(props: { onLegendItemClick: () => void; customColor: string }) { - const data = dg.generateGroupedSeries(10, 4, 'split'); - const [color, setColor] = useState('red'); +export const Playground = () => { + const ref1 = React.createRef(); + const ref2 = React.createRef(); + const ref3 = React.createRef(); + const ref4 = React.createRef(); + + const pointerUpdate = (event: PointerEvent) => { + if (ref1.current) { + ref1.current.dispatchExternalPointerEvent(event); + } + if (ref2.current) { + ref2.current.dispatchExternalPointerEvent(event); + } + if (ref3.current) { + ref3.current.dispatchExternalPointerEvent(event); + } + if (ref4.current) { + ref4.current.dispatchExternalPointerEvent(event); + } + }; return ( - <> +
- - - - - - ); -} - -export class Playground extends React.Component { - render() { - return ( - { - // npo + + + +
+ + + + Number(d).toFixed(2)} /> + Hello
} + dataValues={[{ dataValue: KIBANA_METRICS.metrics.kibana_os_load[0].data[10][0], details: 'hello' }]} + id="test" + domainType={AnnotationDomainTypes.XDomain} + /> + + + +
+ +
+ + + + Number(d).toFixed(2)} /> + + + + +
+ + +
+ + + + Number(d).toFixed(2)} /> + + + + +
+ +
+ + + + Number(d).toFixed(2)} /> + + + + +
+
+ ); +}; diff --git a/api/charts.api.md b/api/charts.api.md index 53f209317e..b919712ea9 100644 --- a/api/charts.api.md +++ b/api/charts.api.md @@ -534,7 +534,7 @@ export const DEFAULT_TOOLTIP_TYPE: "vertical"; // Warning: (ae-missing-release-tag) "DefaultSettingsProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'showLegend' | 'debug' | 'tooltip' | 'showLegendExtra' | 'theme' | 'legendPosition' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta'; +export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'showLegend' | 'debug' | 'tooltip' | 'showLegendExtra' | 'theme' | 'legendPosition' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents'; // Warning: (ae-missing-release-tag) "DisplayValueSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -570,6 +570,16 @@ export type ElementClickListener = (elements: Array) => void; +// @alpha +export interface ExternalPointerEventsSettings { + tooltip: { + visible?: boolean; + placement?: Placement; + fallbackPlacements?: Placement[]; + boundary?: HTMLElement | 'chart'; + }; +} + // Warning: (ae-missing-release-tag) "FillStyle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1329,9 +1339,7 @@ export type SeriesTypes = $Values; // @public (undocumented) export const Settings: React.FunctionComponent; -// Warning: (ae-missing-release-tag) "SettingsSpec" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface SettingsSpec extends Spec { // (undocumented) animateData: boolean; @@ -1339,6 +1347,8 @@ export interface SettingsSpec extends Spec { brushAxis?: BrushAxis; // (undocumented) debug: boolean; + // @alpha + externalPointerEvents: ExternalPointerEventsSettings; flatLegend?: boolean; hideDuplicateAxes: boolean; // (undocumented) @@ -1391,7 +1401,9 @@ export interface SettingsSpec extends Spec { // Warning: (ae-missing-release-tag) "SettingsSpecProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type SettingsSpecProps = Partial>; +export type SettingsSpecProps = Partial> & { + externalPointerEvents?: RecursivePartial; +}; // Warning: (ae-missing-release-tag) "SharedGeometryStateStyle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-cursor-update-action-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-cursor-update-action-visually-looks-correct-1-snap.png index b0923cea76..5112f10000 100644 Binary files a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-cursor-update-action-visually-looks-correct-1-snap.png and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-interactions-cursor-update-action-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltip-sync-show-synced-tooltips-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltip-sync-show-synced-tooltips-1-snap.png new file mode 100644 index 0000000000..14c30a26dd Binary files /dev/null and b/integration/tests/__image_snapshots__/interactions-test-ts-interactions-tooltip-sync-show-synced-tooltips-1-snap.png differ diff --git a/integration/tests/interactions.test.ts b/integration/tests/interactions.test.ts index adc2fe9151..7e7b02d61f 100644 --- a/integration/tests/interactions.test.ts +++ b/integration/tests/interactions.test.ts @@ -209,4 +209,16 @@ describe('Interactions', () => { ); }); }); + + describe('Tooltip sync', () => { + it('show synced tooltips', async() => { + await common.expectChartWithMouseAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/interactions--cursor-update-action', + { left: 180, top: 80 }, + { + screenshotSelector: '#story-root', + } + ); + }); + }); }); diff --git a/src/chart_types/goal_chart/state/chart_state.tsx b/src/chart_types/goal_chart/state/chart_state.tsx index 85306023a9..3c4f1be200 100644 --- a/src/chart_types/goal_chart/state/chart_state.tsx +++ b/src/chart_types/goal_chart/state/chart_state.tsx @@ -93,7 +93,7 @@ export class GoalState implements InternalChartState { } isTooltipVisible(globalState: GlobalChartState) { - return isTooltipVisibleSelector(globalState); + return { visible: isTooltipVisibleSelector(globalState), isExternal: false }; } getTooltipInfo(globalState: GlobalChartState) { diff --git a/src/chart_types/partition_chart/state/chart_state.tsx b/src/chart_types/partition_chart/state/chart_state.tsx index cd2c2a5006..a5045bc2f3 100644 --- a/src/chart_types/partition_chart/state/chart_state.tsx +++ b/src/chart_types/partition_chart/state/chart_state.tsx @@ -95,7 +95,7 @@ export class PartitionState implements InternalChartState { } isTooltipVisible(globalState: GlobalChartState) { - return isTooltipVisibleSelector(globalState); + return { visible: isTooltipVisibleSelector(globalState), isExternal: false }; } getTooltipInfo(globalState: GlobalChartState) { diff --git a/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx b/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx index 0e7eb78d4b..892414cf7a 100644 --- a/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx +++ b/src/chart_types/xy_chart/renderer/dom/annotations/annotation_tooltip.tsx @@ -56,7 +56,9 @@ export const AnnotationTooltip = ({ state, chartRef, chartId, onScroll }: RectAn const position = useMemo(() => state?.anchor ?? null, [state?.anchor]); const placement = useMemo(() => state?.anchor?.position ?? Placement.Right, [state?.anchor?.position]); - + if (!state?.isVisible) { + return null; + } return ( { tooltipType: TooltipType.None, }; } + const settings = getSettingsSpecSelector(state); + const cursorBandPosition = getCursorBandPositionSelector(state); + + const tooltipType = getTooltipType(settings, cursorBandPosition?.fromExternalEvent); return { theme: getChartThemeSelector(state), chartRotation: getChartRotationSelector(state), - cursorBandPosition: getCursorBandPositionSelector(state), + cursorBandPosition, cursorLinePosition: getCursorLinePositionSelector(state), - tooltipType: getTooltipTypeSelector(state), + tooltipType, }; }; diff --git a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts index c461cbd466..c7d81e0199 100644 --- a/src/chart_types/xy_chart/state/chart_state.interactions.test.ts +++ b/src/chart_types/xy_chart/state/chart_state.interactions.test.ts @@ -245,7 +245,7 @@ describe('Chart state pointer interactions', () => { // no tooltip values exist if we have a TooltipType === None expect(tooltipInfo.tooltip.values.length).toBe(0); let isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(false); + expect(isTooltipVisible.visible).toBe(false); updatedSettings = { ...settingSpec, @@ -261,7 +261,7 @@ describe('Chart state pointer interactions', () => { const highlightedGeometries = getHighlightedGeomsSelector(store.getState()); expect(highlightedGeometries.length).toBe(1); isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); }); describe('mouse over with Ordinal scale', () => { @@ -415,7 +415,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 0); expect(cursorBandPosition?.width).toBe(45); let isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.tooltip.values.length).toBe(1); expect(tooltipInfo.highlightedGeometries.length).toBe(1); @@ -443,7 +443,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); expect(projectedPointerPosition).toEqual({ x: -1, y: -1 }); isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(false); + expect(isTooltipVisible.visible).toBe(false); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.tooltip.values.length).toBe(0); expect(tooltipInfo.highlightedGeometries.length).toBe(0); @@ -460,7 +460,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 0); expect(cursorBandPosition?.width).toBe(45); let isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.highlightedGeometries.length).toBe(1); expect(tooltipInfo.tooltip.values.length).toBe(1); @@ -487,7 +487,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { projectedPointerPosition = getProjectedPointerPositionSelector(store.getState()); expect(projectedPointerPosition).toEqual({ x: -1, y: 89 }); isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(false); + expect(isTooltipVisible.visible).toBe(false); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.tooltip.values.length).toBe(0); expect(tooltipInfo.highlightedGeometries.length).toBe(0); @@ -508,7 +508,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 0); expect(cursorBandPosition?.width).toBe(45); let isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.highlightedGeometries.length).toBe(1); expect(tooltipInfo.tooltip.values.length).toBe(1); @@ -540,7 +540,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 45); expect(cursorBandPosition?.width).toBe(45); isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.tooltip.values.length).toBe(1); expect(tooltipInfo.highlightedGeometries.length).toBe(0); @@ -561,7 +561,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 0); expect(cursorBandPosition?.width).toBe(45); let isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); let tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.highlightedGeometries.length).toBe(1); expect(tooltipInfo.tooltip.values.length).toBe(1); @@ -593,7 +593,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 45); expect(cursorBandPosition?.width).toBe(45); isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.tooltip.values.length).toBe(1); // we are over the second bar here @@ -638,7 +638,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.width).toBe(45); const isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.highlightedGeometries.length).toBe(0); expect(tooltipInfo.tooltip.values.length).toBe(1); @@ -690,7 +690,7 @@ function mouseOverTestSuite(scaleType: ScaleType) { expect(cursorBandPosition?.left).toBe(chartLeft + 45); expect(cursorBandPosition?.width).toBe(45); const isTooltipVisible = isTooltipVisibleSelector(store.getState()); - expect(isTooltipVisible).toBe(true); + expect(isTooltipVisible.visible).toBe(true); const tooltipInfo = getTooltipInfoAndGeometriesSelector(store.getState()); expect(tooltipInfo.highlightedGeometries.length).toBe(1); expect(tooltipInfo.tooltip.values.length).toBe(1); diff --git a/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts b/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts index 39d29f9b04..6c7cfdc26f 100644 --- a/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts +++ b/src/chart_types/xy_chart/state/selectors/get_cursor_band.ts @@ -86,7 +86,7 @@ function getCursorBand( totalBarsInCluster: number, isTooltipSnapEnabled: boolean, geometriesIndexKeys: (string | number)[], -): (Dimensions & { visible: boolean }) | undefined { +): (Dimensions & { visible: boolean, fromExternalEvent: boolean }) | undefined { // update che cursorBandPosition based on chart configuration const isLineAreaOnly = isLineAreaOnlyChart(seriesSpecs); if (!xScale) { @@ -94,10 +94,12 @@ function getCursorBand( } let pointerPosition = orientedProjectedPoinerPosition; let xValue; + let fromExternalEvent = false; + // external pointer events takes precendence over the current mouse pointer if (isValidPointerOverEvent(xScale, externalPointerEvent)) { + fromExternalEvent = true; const x = xScale.pureScale(externalPointerEvent.value); - - if (x == null || x > chartDimensions.width + chartDimensions.left) { + if (x == null || x > chartDimensions.width || x < 0) { return; } pointerPosition = { x, y: 0 }; @@ -111,7 +113,8 @@ function getCursorBand( return; } } - return getCursorBandPosition( + + const cursorBand = getCursorBandPosition( settingsSpec.rotation, chartDimensions, pointerPosition, @@ -123,4 +126,8 @@ function getCursorBand( xScale, isLineAreaOnly ? 1 : totalBarsInCluster, ); + return { + ...cursorBand, + fromExternalEvent, + }; } diff --git a/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts b/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts index 721154aa2b..facacd197a 100644 --- a/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts +++ b/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.ts @@ -64,7 +64,7 @@ function getElementAtCursorPosition( if (isValidPointerOverEvent(scales.xScale, externalPointerEvent)) { const x = scales.xScale.pureScale(externalPointerEvent.value); - if (x == null || x > chartDimensions.width + chartDimensions.left) { + if (x == null || x > chartDimensions.width + chartDimensions.left || x < 0) { return []; } // TODO: Handle external event with spatial points diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index c7863a33ce..1b0f1b9a7f 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -27,6 +27,7 @@ import { TooltipValueFormatter, isFollowTooltipType, SettingsSpec, + getTooltipType, } from '../../../../specs'; import { TooltipType } from '../../../../specs/constants'; import { GlobalChartState } from '../../../../state/chart_state'; @@ -48,7 +49,6 @@ import { getElementAtCursorPositionSelector } from './get_elements_at_cursor_pos import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position'; import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; -import { getTooltipTypeSelector } from './get_tooltip_type'; import { hasSingleSeriesSelector } from './has_single_series'; const EMPTY_VALUES = Object.freeze({ @@ -79,7 +79,6 @@ export const getTooltipInfoAndGeometriesSelector = createCachedSelector( hasSingleSeriesSelector, getComputedScalesSelector, getElementAtCursorPositionSelector, - getTooltipTypeSelector, getExternalPointerEventStateSelector, getTooltipHeaderFormatterSelector, ], @@ -96,18 +95,17 @@ function getTooltipAndHighlightFromValue( hasSingleSeries: boolean, scales: ComputedScales, matchingGeoms: IndexedGeometry[], - tooltipType: TooltipType, externalPointerEvent: PointerEvent | null, tooltipHeaderFormatter?: TooltipValueFormatter, ): TooltipAndHighlightedGeoms { if (!scales.xScale || !scales.yScales) { return EMPTY_VALUES; } - if (tooltipType === TooltipType.None) { - return EMPTY_VALUES; - } + let { x, y } = orientedProjectedPointerPosition; + let tooltipType = getTooltipType(settings); if (isValidPointerOverEvent(scales.xScale, externalPointerEvent)) { + tooltipType = getTooltipType(settings, true); const scaledX = scales.xScale.pureScale(externalPointerEvent.value); if (scaledX === null) { @@ -120,6 +118,10 @@ function getTooltipAndHighlightFromValue( return EMPTY_VALUES; } + if (tooltipType === TooltipType.None) { + return EMPTY_VALUES; + } + if (matchingGeoms.length === 0) { return EMPTY_VALUES; } diff --git a/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts b/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts index 35112a3d93..8852bfe0b3 100644 --- a/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts +++ b/src/chart_types/xy_chart/state/selectors/is_tooltip_visible.ts @@ -25,35 +25,39 @@ import { TooltipType } from '../../../../specs/constants'; import { GlobalChartState, PointerStates } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { isExternalTooltipVisibleSelector } from '../../../../state/selectors/is_external_tooltip_visible'; import { Point } from '../../../../utils/point'; import { getProjectedPointerPositionSelector } from './get_projected_pointer_position'; import { getTooltipInfoSelector } from './get_tooltip_values_highlighted_geoms'; import { isAnnotationTooltipVisibleSelector } from './is_annotation_tooltip_visible'; -const hasTooltipTypeDefinedSelector = (state: GlobalChartState): TooltipType | undefined => getTooltipType(getSettingsSpecSelector(state)); +const getTooltipTypeSelector = (state: GlobalChartState): TooltipType => getTooltipType(getSettingsSpecSelector(state)); const getPointerSelector = (state: GlobalChartState) => state.interactions.pointer; + /** @internal */ export const isTooltipVisibleSelector = createCachedSelector( [ - hasTooltipTypeDefinedSelector, + getTooltipTypeSelector, getPointerSelector, getProjectedPointerPositionSelector, getTooltipInfoSelector, isAnnotationTooltipVisibleSelector, + isExternalTooltipVisibleSelector, ], isTooltipVisible, )(getChartIdSelector); function isTooltipVisible( - tooltipType: TooltipType | undefined, + tooltipType: TooltipType, pointer: PointerStates, projectedPointerPosition: Point, tooltip: TooltipInfo, isAnnotationTooltipVisible: boolean, + externalTooltipVisible: boolean, ) { - return ( + const isLocalTooltop = ( tooltipType !== TooltipType.None && pointer.down === null && projectedPointerPosition.x > -1 @@ -61,4 +65,9 @@ function isTooltipVisible( && tooltip.values.length > 0 && !isAnnotationTooltipVisible ); + const isExternalTooltip = externalTooltipVisible && tooltip.values.length > 0; + return { + visible: isLocalTooltop || isExternalTooltip, + isExternal: externalTooltipVisible, + }; } diff --git a/src/chart_types/xy_chart/state/utils/utils.ts b/src/chart_types/xy_chart/state/utils/utils.ts index d8eb585365..37660b5945 100644 --- a/src/chart_types/xy_chart/state/utils/utils.ts +++ b/src/chart_types/xy_chart/state/utils/utils.ts @@ -482,10 +482,7 @@ function renderGeometries( const valueFormatter = yAxis && yAxis.tickFormat ? yAxis.tickFormat : identity; const displayValueSettings = spec.displayValueSettings - ? { - valueFormatter, - ...spec.displayValueSettings, - } + ? { valueFormatter, ...spec.displayValueSettings } : undefined; const renderedBars = renderBars( diff --git a/src/components/__snapshots__/chart.test.tsx.snap b/src/components/__snapshots__/chart.test.tsx.snap index 00df8cd9ad..7b12935f4f 100644 --- a/src/components/__snapshots__/chart.test.tsx.snap +++ b/src/components/__snapshots__/chart.test.tsx.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Chart should render the legend name test 1`] = `"
  • test
"`; +exports[`Chart should render the legend name test 1`] = `"
  • test
"`; diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index c5a0340921..b536fa35c5 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -68,57 +68,49 @@ interface LegendDispatchProps { setTemporaryColor: typeof setTemporaryColor; setPersistedColor: typeof setPersistedColor; } -type LegendProps = LegendStateProps & LegendDispatchProps; -/** - * @internal - */ -export class LegendComponent extends React.Component { - static displayName = 'Legend'; - - render() { - const { - items, - position, - size, - debug, - chartTheme: { chartMargins, legend }, - } = this.props; - if (items.length === 0) { - return null; - } - const legendContainerStyle = getLegendStyle(position, size); - const legendListStyle = getLegendListStyle(position, chartMargins, legend); - const legendClasses = classNames('echLegend', `echLegend--${position}`, { - 'echLegend--debug': debug, - }); +function LegendComponent(props: LegendStateProps & LegendDispatchProps) { + const { + items, + position, + size, + debug, + chartTheme: { chartMargins, legend }, + } = props; + if (items.length === 0) { + return null; + } + const legendContainerStyle = getLegendStyle(position, size); + const legendListStyle = getLegendListStyle(position, chartMargins, legend); + const legendClasses = classNames('echLegend', `echLegend--${position}`, { + 'echLegend--debug': debug, + }); - const itemProps: Omit = { - position, - totalItems: items.length, - extraValues: this.props.extraValues, - showExtra: this.props.showExtra, - onMouseOut: this.props.onItemOut, - onMouseOver: this.props.onItemOver, - onClick: this.props.onItemClick, - clearTemporaryColorsAction: this.props.clearTemporaryColors, - setPersistedColorAction: this.props.setPersistedColor, - setTemporaryColorAction: this.props.setTemporaryColor, - mouseOutAction: this.props.onItemOutAction, - mouseOverAction: this.props.onItemOverAction, - toggleDeselectSeriesAction: this.props.onToggleDeselectSeriesAction, - colorPicker: this.props.colorPicker, - }; - return ( -
-
-
    - {items.map((item, index) => renderLegendItem(item, itemProps, items.length, index))} -
-
+ const itemProps: Omit = { + position, + totalItems: items.length, + extraValues: props.extraValues, + showExtra: props.showExtra, + onMouseOut: props.onItemOut, + onMouseOver: props.onItemOver, + onClick: props.onItemClick, + clearTemporaryColorsAction: props.clearTemporaryColors, + setPersistedColorAction: props.setPersistedColor, + setTemporaryColorAction: props.setTemporaryColor, + mouseOutAction: props.onItemOutAction, + mouseOverAction: props.onItemOverAction, + toggleDeselectSeriesAction: props.onToggleDeselectSeriesAction, + colorPicker: props.colorPicker, + }; + return ( +
+
+
    + {items.map((item, index) => renderLegendItem(item, itemProps, items.length, index))} +
- ); - } +
+ ); } const mapDispatchToProps = (dispatch: Dispatch): LegendDispatchProps => @@ -145,6 +137,7 @@ const EMPTY_DEFAULT_STATE = { size: { width: 0, height: 0 }, showExtra: false, }; + const mapStateToProps = (state: GlobalChartState): LegendStateProps => { if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { return EMPTY_DEFAULT_STATE; diff --git a/src/components/portal/_portal.scss b/src/components/portal/_portal.scss index 90e6ecdc71..aa799d9276 100644 --- a/src/components/portal/_portal.scss +++ b/src/components/portal/_portal.scss @@ -11,4 +11,6 @@ .echTooltipPortal__invisible { position: fixed; visibility: hidden; + width: 0; + height: 0; } diff --git a/src/components/portal/tooltip_portal.tsx b/src/components/portal/tooltip_portal.tsx index 1a7a19ff05..573ab14716 100644 --- a/src/components/portal/tooltip_portal.tsx +++ b/src/components/portal/tooltip_portal.tsx @@ -18,8 +18,7 @@ */ import { createPopper, Instance } from '@popperjs/core'; -import classNames from 'classnames'; -import React, { useRef, useEffect, useCallback, ReactNode, useMemo, useState } from 'react'; +import { useRef, useEffect, useCallback, ReactNode, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { mergePartial, isDefined } from '../../utils/commons'; @@ -57,22 +56,21 @@ type PortalTooltipProps = { }; const TooltipPortalComponent = ({ anchor, scope, settings, children, visible, chartId }: PortalTooltipProps) => { - /** - * Used to skip first render for new position, which is used for capture initial position - */ - const [invisible, setInvisible] = useState(!(visible ?? false)); /** * Anchor element used to position tooltip */ - const anchorNode = useRef( - isHTMLElement(anchor) ? anchor : getOrCreateNode(`echAnchor${scope}__${chartId}`, anchor?.ref ?? undefined), + const anchorNode = useRef(isHTMLElement(anchor) + ? anchor + : getOrCreateNode(`echAnchor${scope}__${chartId}`, undefined, anchor?.ref ?? undefined), ); /** * This must not be removed from DOM throughout life of this component. * Otherwise the portal will loose reference to the correct node. */ - const portalNode = useRef(getOrCreateNode(`echTooltipPortal${scope}`)); + const portalNodeElement = getOrCreateNode(`echTooltipPortal${scope}__${chartId}`, 'echTooltipPortal__invisible'); + + const portalNode = useRef(portalNodeElement); /** * Popper instance used to manage position of tooltip. @@ -81,6 +79,7 @@ const TooltipPortalComponent = ({ anchor, scope, settings, children, visible, ch const popperSettings = useMemo( () => mergePartial(DEFAULT_POPPER_SETTINGS, settings, { mergeOptionalPartialValues: true }), + [settings], ); @@ -185,11 +184,11 @@ const TooltipPortalComponent = ({ anchor, scope, settings, children, visible, ch }, [visible, anchorNode, position?.left, position?.top, position?.width, position?.height]); useEffect(() => { - if (position === null) { - setInvisible(true); - } else { - setInvisible(false); + if (!position) { + portalNode.current.classList.add('echTooltipPortal__invisible'); + return; } + portalNode.current.classList.remove('echTooltipPortal__invisible'); }, [position]); useEffect(() => { @@ -199,12 +198,7 @@ const TooltipPortalComponent = ({ anchor, scope, settings, children, visible, ch } }, [updateAnchorDimensions, popper]); - return createPortal( -
- {children} -
, - portalNode.current, - ); + return createPortal(children, portalNode.current); }; TooltipPortalComponent.displayName = 'TooltipPortal'; diff --git a/src/components/portal/utils.ts b/src/components/portal/utils.ts index 7d5dc87dc3..f46b653dae 100644 --- a/src/components/portal/utils.ts +++ b/src/components/portal/utils.ts @@ -31,7 +31,7 @@ export const DEFAULT_POPPER_SETTINGS: PopperSettings = { * * @internal */ -export function getOrCreateNode(id: string, parent: HTMLElement = document.body): HTMLDivElement { +export function getOrCreateNode(id: string, className?: string, parent: HTMLElement = document.body): HTMLDivElement { // eslint-disable-next-line unicorn/prefer-query-selector const node = document.getElementById(id); if (node) { @@ -40,6 +40,9 @@ export function getOrCreateNode(id: string, parent: HTMLElement = document.body) const newNode = document.createElement('div'); newNode.id = id; + if (className) { + newNode.classList.add(className); + } parent.appendChild(newNode); return newNode; } diff --git a/src/components/tooltip/get_tooltip_settings.ts b/src/components/tooltip/get_tooltip_settings.ts new file mode 100644 index 0000000000..6b134e80e3 --- /dev/null +++ b/src/components/tooltip/get_tooltip_settings.ts @@ -0,0 +1,37 @@ +/* + * 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 { TooltipSettings, isTooltipType, SettingsSpec } from '../../specs/settings'; + +/** @internal */ +export function getTooltipSettings(settings: SettingsSpec, isExternalTooltipVisible: boolean): TooltipSettings { + if (!isExternalTooltipVisible) { + return settings.tooltip; + } + if (isTooltipType(settings.tooltip)) { + return { + type: settings.tooltip, + ...settings.externalPointerEvents.tooltip, + }; + } + return { + ...settings.tooltip, + ...settings.externalPointerEvents.tooltip, + }; +} diff --git a/src/components/tooltip/tooltip.tsx b/src/components/tooltip/tooltip.tsx index 9a8f9703f1..37b2ca7449 100644 --- a/src/components/tooltip/tooltip.tsx +++ b/src/components/tooltip/tooltip.tsx @@ -23,7 +23,7 @@ import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; import { TooltipValueFormatter, TooltipSettings, TooltipValue } from '../../specs'; -import { onPointerMove } from '../../state/actions/mouse'; +import { onPointerMove as onPointerMoveAction } from '../../state/actions/mouse'; import { GlobalChartState, BackwardRef } from '../../state/chart_state'; import { getChartRotationSelector } from '../../state/selectors/get_chart_rotation'; import { getChartThemeSelector } from '../../state/selectors/get_chart_theme'; @@ -35,14 +35,16 @@ import { getSettingsSpecSelector } from '../../state/selectors/get_settings_spec import { getTooltipHeaderFormatterSelector } from '../../state/selectors/get_tooltip_header_formatter'; import { Rotation } from '../../utils/commons'; import { TooltipPortal, PopperSettings, AnchorPosition, Placement } from '../portal'; +import { getTooltipSettings } from './get_tooltip_settings'; import { TooltipInfo, TooltipAnchorPosition } from './types'; + interface TooltipDispatchProps { - onPointerMove: typeof onPointerMove; + onPointerMove: typeof onPointerMoveAction; } interface TooltipStateProps { - isVisible: boolean; + visible: boolean; position: TooltipAnchorPosition | null; info?: TooltipInfo; headerFormatter?: TooltipValueFormatter; @@ -64,7 +66,7 @@ const TooltipComponent = ({ position, getChartContainerRef, settings, - isVisible, + visible, rotation, chartId, onPointerMove, @@ -138,7 +140,7 @@ const TooltipComponent = ({ ); const renderTooltip = () => { - if (!info || !isVisible) { + if (!info || !visible) { return null; } @@ -156,7 +158,7 @@ const TooltipComponent = ({ }; const anchorPosition = useMemo((): AnchorPosition | null => { - if (!position || !isVisible) { + if (!position || !visible) { return null; } @@ -169,7 +171,7 @@ const TooltipComponent = ({ top: y1 - height, height, }; - }, [isVisible, position?.x0, position?.x1, position?.y0, position?.y1]); // eslint-disable-line react-hooks/exhaustive-deps + }, [visible, position?.x0, position?.x1, position?.y0, position?.y1]); // eslint-disable-line react-hooks/exhaustive-deps const popperSettings = useMemo((): Partial | undefined => { if (typeof settings === 'string') { @@ -189,7 +191,9 @@ const TooltipComponent = ({ boundary: boundary === 'chart' && chartRef.current ? chartRef.current : undefined, }; }, [settings, chartRef, rotation]); - + if (!visible) { + return null; + } return ( {renderTooltip()} @@ -209,7 +213,7 @@ const TooltipComponent = ({ TooltipComponent.displayName = 'Tooltip'; const HIDDEN_TOOLTIP_PROPS = { - isVisible: false, + visible: false, info: undefined, position: null, headerFormatter: undefined, @@ -220,18 +224,24 @@ const HIDDEN_TOOLTIP_PROPS = { }; const mapDispatchToProps = (dispatch: Dispatch): TooltipDispatchProps => - bindActionCreators({ onPointerMove }, dispatch); + bindActionCreators({ onPointerMove: onPointerMoveAction }, dispatch); const mapStateToProps = (state: GlobalChartState): TooltipStateProps => { if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) { return HIDDEN_TOOLTIP_PROPS; } + const { visible, isExternal } = getInternalIsTooltipVisibleSelector(state); + if (state.chartId === 'chart4') { + // console.log(visible, isExternal); + } + const settingsSpec = getSettingsSpecSelector(state); + const settings = getTooltipSettings(settingsSpec, isExternal); return { - isVisible: getInternalIsTooltipVisibleSelector(state), + visible, info: getInternalTooltipInfoSelector(state), position: getInternalTooltipAnchorPositionSelector(state), headerFormatter: getTooltipHeaderFormatterSelector(state), - settings: getSettingsSpecSelector(state).tooltip, + settings, rotation: getChartRotationSelector(state), chartId: state.chartId, backgroundColor: getChartThemeSelector(state).background.color, diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts index eb72f7ec30..0f330876ba 100644 --- a/src/mocks/specs/specs.ts +++ b/src/mocks/specs/specs.ts @@ -41,7 +41,7 @@ import { AxisSpec, } from '../../chart_types/xy_chart/utils/specs'; import { ScaleType } from '../../scales/constants'; -import { SettingsSpec, SpecTypes, TooltipType } from '../../specs'; +import { SettingsSpec, SpecTypes, DEFAULT_SETTINGS_SPEC } from '../../specs'; import { Datum, mergePartial, Position, RecursivePartial } from '../../utils/commons'; import { LIGHT_THEME } from '../../utils/themes/light_theme'; @@ -267,25 +267,7 @@ export class MockSeriesSpecs { /** @internal */ export class MockGlobalSpec { - private static readonly settingsBase: SettingsSpec = { - id: '__global__settings___', - chartType: ChartTypes.Global, - specType: SpecTypes.Settings, - rendering: 'canvas' as const, - rotation: 0 as const, - animateData: true, - showLegend: false, - resizeDebounce: 10, - debug: false, - tooltip: { - type: TooltipType.VerticalCursor, - snap: true, - }, - legendPosition: Position.Right, - showLegendExtra: true, - hideDuplicateAxes: false, - theme: LIGHT_THEME, - }; + private static readonly settingsBase: SettingsSpec = DEFAULT_SETTINGS_SPEC; private static readonly axisBase: AxisSpec = { id: 'yAxis', diff --git a/src/specs/constants.ts b/src/specs/constants.ts index f045db411f..4ec732533f 100644 --- a/src/specs/constants.ts +++ b/src/specs/constants.ts @@ -93,6 +93,11 @@ export const DEFAULT_SETTINGS_SPEC: SettingsSpec = { type: DEFAULT_TOOLTIP_TYPE, snap: DEFAULT_TOOLTIP_SNAP, }, + externalPointerEvents: { + tooltip: { + visible: false, + }, + }, legendPosition: Position.Right, showLegendExtra: false, hideDuplicateAxes: false, diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 8cf37edde0..b5b568dea1 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -29,7 +29,7 @@ import { CustomTooltip } from '../components/tooltip/types'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; import { getConnect, specComponentFactory } from '../state/spec_factory'; import { Accessor } from '../utils/accessor'; -import { Position, Rendering, Rotation, Color } from '../utils/commons'; +import { Position, Rendering, Rotation, Color, RecursivePartial } from '../utils/commons'; import { Domain } from '../utils/domain'; import { GeometryValue } from '../utils/geometry'; import { GroupId } from '../utils/ids'; @@ -171,7 +171,7 @@ export interface TooltipProps { * * `'chart'` will use the chart container as the boundary * - * @defaultValue parent scroll container + * @defaultValue undefined - parent scroll container */ boundary?: HTMLElement | 'chart'; /** @@ -187,11 +187,42 @@ export interface TooltipProps { } /** - * Either a TooltipType or an object with configuration of type, snap, and/or headerFormatter + * Either a {@link (TooltipType:type)} or an {@link (TooltipProps:interface)} configuration * @public */ export type TooltipSettings = TooltipType | TooltipProps; +/** + * The settings for handling external events. + * @alpha + */ +export interface ExternalPointerEventsSettings { + /** + * Tooltip settings used for external events + */ + tooltip: { + /** + * `true` to show the tooltip when the chart receive an + * external pointer event, 'false' to hide the tooltip. + * @defaultValue `false` + */ + visible?: boolean; + /** + * {@inheritDoc TooltipProps.placement} + */ + placement?: Placement; + /** + * {@inheritDoc TooltipProps.fallbackPlacements} + */ + fallbackPlacements?: Placement[]; + /** + * {@inheritDoc TooltipProps.boundary} + */ + boundary?: HTMLElement | 'chart'; + } + +} + export interface LegendColorPickerProps { /** * Anchor used to position picker @@ -221,6 +252,10 @@ export type LegendColorPicker = React.ComponentType; */ export type MarkBuffer = number | ((radius: number) => number); +/** + * The Spec used for Chart settings + * @public + */ export interface SettingsSpec extends Spec { /** * Partial theme to be merged with base @@ -244,9 +279,14 @@ export interface SettingsSpec extends Spec { animateData: boolean; showLegend: boolean; /** - * The tooltip configuration forr the chart {@link TooltipSettings} + * The tooltip configuration {@link TooltipSettings} */ tooltip: TooltipSettings; + /** + * {@inheritDoc ExternalPointerEventsSettings} + * @alpha + */ + externalPointerEvents: ExternalPointerEventsSettings; debug: boolean; legendPosition: Position; /** @@ -312,9 +352,12 @@ export type DefaultSettingsProps = | 'legendPosition' | 'hideDuplicateAxes' | 'brushAxis' - | 'minBrushDelta'; + | 'minBrushDelta' + | 'externalPointerEvents'; -export type SettingsSpecProps = Partial>; +export type SettingsSpecProps = Partial> & { + externalPointerEvents?: RecursivePartial +}; export const Settings: React.FunctionComponent = getConnect()( specComponentFactory(DEFAULT_SETTINGS_SPEC), @@ -351,8 +394,11 @@ export function isFollowTooltipType(type: TooltipType) { } /** @internal */ -export function getTooltipType(settings: SettingsSpec): TooltipType { +export function getTooltipType(settings: SettingsSpec, externalTooltip = false): TooltipType { const defaultType = TooltipType.VerticalCursor; + if (externalTooltip) { + return getExternalTooltipType(settings); + } const { tooltip } = settings; if (tooltip === undefined || tooltip === null) { return defaultType; @@ -365,3 +411,13 @@ export function getTooltipType(settings: SettingsSpec): TooltipType { } return defaultType; } + + +/** + * Always return a Vertical Cursor for external pointer events or None if hidden + * @internal + * @param settings - the SettingsSpec + */ +export function getExternalTooltipType({ externalPointerEvents: { tooltip: { visible } } }: SettingsSpec): TooltipType { + return visible ? TooltipType.VerticalCursor : TooltipType.None; +} diff --git a/src/state/chart_state.ts b/src/state/chart_state.ts index 603eeb33ad..7b4469395d 100644 --- a/src/state/chart_state.ts +++ b/src/state/chart_state.ts @@ -100,10 +100,10 @@ export interface InternalChartState { */ getPointerCursor(globalState: GlobalChartState): string; /** - * `true` if the tooltip is visible, `false` otherwise + * Describe if the tooltip is visible and comes from an external source * @param globalState */ - isTooltipVisible(globalState: GlobalChartState): boolean; + isTooltipVisible(globalState: GlobalChartState): { visible: boolean, isExternal: boolean }; /** * Get the tooltip information to display * @param globalState the GlobalChartState diff --git a/src/state/selectors/get_internal_is_tooltip_visible.ts b/src/state/selectors/get_internal_is_tooltip_visible.ts index dfb76c0011..659ac4aef8 100644 --- a/src/state/selectors/get_internal_is_tooltip_visible.ts +++ b/src/state/selectors/get_internal_is_tooltip_visible.ts @@ -20,9 +20,9 @@ import { GlobalChartState } from '../chart_state'; /** @internal */ -export const getInternalIsTooltipVisibleSelector = (state: GlobalChartState): boolean => { +export const getInternalIsTooltipVisibleSelector = (state: GlobalChartState): { visible: boolean, isExternal: boolean } => { if (state.internalChartState) { return state.internalChartState.isTooltipVisible(state); } - return false; + return { visible: false, isExternal: false }; }; diff --git a/src/state/selectors/has_external_pointer_event.ts b/src/state/selectors/has_external_pointer_event.ts new file mode 100644 index 0000000000..5fb6e4b701 --- /dev/null +++ b/src/state/selectors/has_external_pointer_event.ts @@ -0,0 +1,25 @@ +/* + * 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 { PointerEventType } from '../../specs'; +import { GlobalChartState } from '../chart_state'; + +/** @internal */ +export const hasExternalEventSelector = ({ externalEvents: { pointer } }: GlobalChartState) => + pointer !== null && pointer.type !== PointerEventType.Out; diff --git a/src/state/selectors/is_external_tooltip_visible.ts b/src/state/selectors/is_external_tooltip_visible.ts new file mode 100644 index 0000000000..be96e16054 --- /dev/null +++ b/src/state/selectors/is_external_tooltip_visible.ts @@ -0,0 +1,46 @@ +/* + * 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 createCachedSelector from 're-reselect'; + +import { computeChartDimensionsSelector } from '../../chart_types/xy_chart/state/selectors/compute_chart_dimensions'; +import { getComputedScalesSelector } from '../../chart_types/xy_chart/state/selectors/get_computed_scales'; +import { PointerEventType } from '../../specs'; +import { GlobalChartState } from '../chart_state'; +import { getChartIdSelector } from './get_chart_id'; +import { getSettingsSpecSelector } from './get_settings_specs'; +import { hasExternalEventSelector } from './has_external_pointer_event'; + +const getExternalEventPointer = ({ externalEvents: { pointer } }: GlobalChartState) => pointer; + +/** @internal */ +export const isExternalTooltipVisibleSelector = createCachedSelector( + [getSettingsSpecSelector, hasExternalEventSelector, getExternalEventPointer, getComputedScalesSelector, computeChartDimensionsSelector], + ({ externalPointerEvents }, hasExternalEvent, pointer, { xScale }, { chartDimensions }): boolean => { + if (!pointer || pointer.type !== PointerEventType.Over || externalPointerEvents.tooltip?.visible === false) { + return false; + } + const x = xScale.pureScale(pointer.value); + + if (x == null || x > chartDimensions.width + chartDimensions.left || x < 0) { + return false; + } + return hasExternalEvent && externalPointerEvents.tooltip?.visible === true; + } +)(getChartIdSelector); diff --git a/stories/interactions/16_cursor_update_action.tsx b/stories/interactions/16_cursor_update_action.tsx index ea7c934800..74705e7c72 100644 --- a/stories/interactions/16_cursor_update_action.tsx +++ b/stories/interactions/16_cursor_update_action.tsx @@ -20,35 +20,87 @@ import { action } from '@storybook/addon-actions'; import React from 'react'; -import { Axis, BarSeries, Chart, Position, ScaleType, Settings } from '../../src'; +import { Axis, BarSeries, Chart, Position, ScaleType, Settings, PointerEvent, Placement, niceTimeFormatter } from '../../src'; +import { KIBANA_METRICS } from '../../src/utils/data_samples/test_dataset_kibana'; +import { palettes } from '../../src/utils/themes/colors'; +import { SB_SOURCE_PANEL } from '../utils/storybook'; -const onPointerUpdate = action('onPointerUpdate'); +export const Example = () => { + const ref1 = React.createRef(); + const ref2 = React.createRef(); + const pointerUpdate = (event: PointerEvent) => { + action('onPointerUpdate')(event); + if (ref1.current) { + ref1.current.dispatchExternalPointerEvent(event); + } + if (ref2.current) { + ref2.current.dispatchExternalPointerEvent(event); + } + }; + const { data } = KIBANA_METRICS.metrics.kibana_os_load[0]; + const data1 = KIBANA_METRICS.metrics.kibana_os_load[0].data; + const data2 = KIBANA_METRICS.metrics.kibana_os_load[1].data; + return ( + <> + + + + Number(d).toFixed(2)} /> -export const Example = () => ( - - - - Number(d).toFixed(2)} /> + + + + + - -); + /> + Number(d).toFixed(2)} + domain={{ min: 5, max: 20 }} + /> + + + + + ); +}; Example.story = { parameters: { info: { text: 'Sends an event every time the cursor changes. This is provided to sync cursors between multiple charts.', }, + options: { selectedPanel: SB_SOURCE_PANEL }, }, };