Skip to content

Commit 809a0ef

Browse files
authored
[ML] Migrates chart tooltip to react. (#48122) (#48264)
- The tooltip styling, code and features have been updated to be more in line with Elastic Charts tooltips. This prepares us should we want to use Elastic Charts' tooltip as is at some point. - jQuery is no longer used in mlChartTooltipService. - The tooltip content is no longer raw HTML, a mostly Elastic Charts compatible data structure is now passed around by mlChartTooltipService.
1 parent 90788a9 commit 809a0ef

File tree

20 files changed

+505
-377
lines changed

20 files changed

+505
-377
lines changed
Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
1+
@import '@elastic/eui/src/components/tool_tip/variables';
2+
@import '@elastic/eui/src/components/tool_tip/mixins';
3+
14
.mlChartTooltip {
5+
@include euiToolTipStyle('s');
6+
@include euiFontSizeXS;
27
position: absolute;
3-
border: 2px solid $euiColorDarkestShade;
4-
border-radius: 5px;
5-
padding: 7px 5px;
6-
color: $euiColorEmptyShade;
7-
background-color: $euiColorDarkestShade;
8-
font-family: $euiFontFamily;
9-
font-size: $euiFontSizeXS;
10-
opacity: 0;
11-
display: none;
12-
white-space: nowrap;
13-
z-index: 1;
14-
transition: opacity 0.2s;
15-
z-index: 20;
16-
max-width: 500px;
17-
overflow: hidden;
18-
text-overflow: ellipsis;
19-
20-
hr {
21-
margin-top: $euiSizeXS;
22-
margin-bottom: $euiSizeXS;
23-
border: none;
24-
height: 1px;
25-
background-color: $euiColorMediumShade;
8+
padding: 0;
9+
transition: opacity $euiAnimSpeedNormal;
10+
pointer-events: none;
11+
user-select: none;
12+
max-width: 512px;
13+
14+
&__list {
15+
margin: $euiSizeXS;
16+
}
17+
18+
&__header {
19+
@include euiToolTipTitle;
20+
padding: $euiSizeXS ($euiSizeXS * 2);
21+
}
22+
23+
&__item {
24+
display: flex;
25+
padding: 3px;
26+
box-sizing: border-box;
27+
border-left: $euiSizeXS solid transparent;
28+
}
29+
30+
&__label {
31+
overflow-wrap: break-word;
32+
word-wrap: break-word;
33+
min-width: 1px;
34+
flex: 1 1 auto;
35+
}
36+
37+
&__value {
38+
overflow-wrap: break-word;
39+
word-wrap: break-word;
40+
font-weight: $euiFontWeightBold;
41+
text-align: right;
42+
font-feature-settings: 'tnum';
43+
margin-left: 8px;
44+
}
45+
46+
&__rowHighlighted {
47+
background-color: transparentize($euiColorGhost, 0.9);
48+
}
49+
50+
&--hidden {
51+
opacity: 0;
2652
}
27-
}
2853

29-
.mlChartTooltip--noTransition {
30-
transition: opacity 0s;
3154
}

x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.html

Lines changed: 0 additions & 1 deletion
This file was deleted.

x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip.js

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import classNames from 'classnames';
8+
import React, { useRef, FC } from 'react';
9+
import { TooltipValueFormatter } from '@elastic/charts';
10+
11+
// TODO: Below import is temporary, use `react-use` lib instead.
12+
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
13+
import { useObservable } from '../../../../../../../src/plugins/kibana_react/public/util/use_observable';
14+
15+
import { chartTooltip$, mlChartTooltipService, ChartTooltipValue } from './chart_tooltip_service';
16+
17+
const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFormatter) => {
18+
if (!headerData) {
19+
return null;
20+
}
21+
22+
return formatter ? formatter(headerData) : headerData.name;
23+
};
24+
25+
export const ChartTooltip: FC = () => {
26+
const chartTooltipElement = useRef(null);
27+
28+
mlChartTooltipService.element = chartTooltipElement.current;
29+
30+
const chartTooltipState = useObservable(chartTooltip$);
31+
32+
if (chartTooltipState === undefined || !chartTooltipState.isTooltipVisible) {
33+
return <div className="mlChartTooltip mlChartTooltip--hidden" ref={chartTooltipElement} />;
34+
}
35+
36+
const { tooltipData, tooltipHeaderFormatter, tooltipPosition } = chartTooltipState;
37+
38+
return (
39+
<div
40+
className="mlChartTooltip"
41+
style={{ transform: tooltipPosition.transform }}
42+
ref={chartTooltipElement}
43+
>
44+
{tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && (
45+
<div className="mlChartTooltip__header">
46+
{renderHeader(tooltipData[0], tooltipHeaderFormatter)}
47+
</div>
48+
)}
49+
{tooltipData.length > 1 && (
50+
<div className="mlChartTooltip__list">
51+
{tooltipData
52+
.slice(1)
53+
.map(({ name, value, color, isHighlighted, seriesKey, yAccessor }) => {
54+
const classes = classNames('mlChartTooltip__item', {
55+
/* eslint @typescript-eslint/camelcase:0 */
56+
echTooltip__rowHighlighted: isHighlighted,
57+
});
58+
return (
59+
<div
60+
key={`${seriesKey}--${yAccessor}`}
61+
className={classes}
62+
style={{
63+
borderLeftColor: color,
64+
}}
65+
>
66+
<span className="mlChartTooltip__label">{name}</span>
67+
<span className="mlChartTooltip__value">{value}</span>
68+
</div>
69+
);
70+
})}
71+
</div>
72+
)}
73+
</div>
74+
);
75+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { BehaviorSubject } from 'rxjs';
8+
9+
import { TooltipValue, TooltipValueFormatter } from '@elastic/charts';
10+
11+
export interface ChartTooltipValue extends TooltipValue {
12+
skipHeader?: boolean;
13+
}
14+
15+
interface ChartTooltipState {
16+
isTooltipVisible: boolean;
17+
tooltipData: ChartTooltipValue[];
18+
tooltipHeaderFormatter?: TooltipValueFormatter;
19+
tooltipPosition: { transform: string };
20+
}
21+
22+
export declare const chartTooltip$: BehaviorSubject<ChartTooltipState>;
23+
24+
interface ToolTipOffset {
25+
x: number;
26+
y: number;
27+
}
28+
29+
interface MlChartTooltipService {
30+
element: HTMLElement | null;
31+
show: (
32+
tooltipData: ChartTooltipValue[],
33+
target: HTMLElement | null,
34+
offset: ToolTipOffset
35+
) => void;
36+
hide: () => void;
37+
}
38+
39+
export declare const mlChartTooltipService: MlChartTooltipService;

x-pack/legacy/plugins/ml/public/components/chart_tooltip/chart_tooltip_service.js

Lines changed: 40 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,86 +4,76 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { BehaviorSubject } from 'rxjs';
78

9+
const doc = document.documentElement;
810

9-
import $ from 'jquery';
11+
const chartTooltipDefaultState = {
12+
isTooltipVisible: false,
13+
tooltipData: [],
14+
tooltipPosition: { transform: 'translate(0px, 0px)' }
15+
};
1016

11-
const doc = document.documentElement;
12-
const FADE_TIMEOUT_MS = 200;
17+
export const chartTooltip$ = new BehaviorSubject(chartTooltipDefaultState);
1318

1419
export const mlChartTooltipService = {
1520
element: null,
16-
fadeTimeout: null,
17-
visible: false
1821
};
1922

20-
mlChartTooltipService.show = function (contents, target, offset = { x: 0, y: 0 }) {
23+
mlChartTooltipService.show = function (tooltipData, target, offset = { x: 0, y: 0 }) {
2124
if (this.element === null || typeof target === 'undefined') {
2225
return;
2326
}
2427

25-
this.visible = true;
26-
// if a previous fade out was happening, stop it
27-
if (this.fadeTimeout !== null) {
28-
clearTimeout(this.fadeTimeout);
28+
// side bar width
29+
const euiNavDrawer = document.getElementsByClassName('euiNavDrawer');
30+
31+
if (euiNavDrawer.length === 0) {
32+
return;
2933
}
3034

31-
// populate the tooltip contents
32-
this.element.html(contents);
35+
// enable the tooltip to render it in the DOM
36+
// so the correct `tooltipWidth` gets returned.
37+
const tooltipState = {
38+
...chartTooltipDefaultState,
39+
isTooltipVisible: true,
40+
tooltipData,
41+
};
42+
chartTooltip$.next(tooltipState);
3343

34-
// side bar width
35-
const navOffset = $('.euiNavDrawer').width() || 0; // Offset by width of side navbar
36-
const contentWidth = $('body').width() - navOffset;
37-
const tooltipWidth = this.element.width();
44+
const navOffset = euiNavDrawer[0].clientWidth; // Offset by width of side navbar
45+
const contentWidth = document.body.clientWidth - navOffset;
46+
const tooltipWidth = this.element.clientWidth;
3847

3948
const pos = target.getBoundingClientRect();
40-
let left = (pos.left + (offset.x) + 4) - navOffset;
49+
let left = pos.left + offset.x + 4 - navOffset;
4150
if (left + tooltipWidth > contentWidth) {
4251
// the tooltip is hanging off the side of the page,
4352
// so move it to the other side of the target
44-
const markerWidthAdjustment = 22;
53+
const markerWidthAdjustment = 10;
4554
left = left - (tooltipWidth + offset.x + markerWidthAdjustment);
4655
}
4756

4857
// Calculate top offset
4958
const scrollTop = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
50-
const topNavHeightAdjustment = 75;
51-
const top = pos.top + (offset.y) + scrollTop - topNavHeightAdjustment;
59+
const topNavHeightAdjustment = 190;
60+
const top = pos.top + offset.y + scrollTop - topNavHeightAdjustment;
5261

53-
this.element.css({
54-
left,
55-
top,
56-
opacity: '0.9',
57-
display: 'block'
62+
// render the tooltip with adjusted position.
63+
chartTooltip$.next({
64+
...tooltipState,
65+
tooltipPosition: { transform: `translate(${left}px, ${top}px)` }
5866
});
67+
5968
};
6069

61-
// When selecting multiple cells using dargSelect, we need to quickly
70+
// When selecting multiple cells using dragSelect, we need to quickly
6271
// hide the tooltip with `noTransition`, otherwise, if the mouse pointer
6372
// enters the tooltip while dragging, it will cancel selecting multiple
6473
// swimlane cells which we'd like to avoid of course.
65-
mlChartTooltipService.hide = function (noTransition = false) {
66-
if (this.element === null) {
67-
return;
68-
}
69-
70-
this.visible = false;
71-
72-
if (noTransition) {
73-
this.element.addClass('mlChartTooltip--noTransition');
74-
this.element.css({ opacity: '0', display: 'none' });
75-
this.element.removeClass('mlChartTooltip--noTransition');
76-
return;
77-
}
78-
79-
this.element.css({ opacity: '0' });
80-
81-
// after the fade out transition has finished, set the display to
82-
// none so it doesn't block any mouse events underneath it.
83-
this.fadeTimeout = setTimeout(() => {
84-
if (this.visible === false) {
85-
this.element.css('display', 'none');
86-
}
87-
this.fadeTimeout = null;
88-
}, FADE_TIMEOUT_MS);
74+
mlChartTooltipService.hide = function () {
75+
chartTooltip$.next({
76+
...chartTooltipDefaultState,
77+
isTooltipVisible: false
78+
});
8979
};

x-pack/legacy/plugins/ml/public/components/chart_tooltip/index.js renamed to x-pack/legacy/plugins/ml/public/components/chart_tooltip/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
8-
9-
import './chart_tooltip';
7+
export { mlChartTooltipService } from './chart_tooltip_service';
8+
export { ChartTooltip } from './chart_tooltip';

x-pack/legacy/plugins/ml/public/datavisualizer/index_based/components/field_data_card/metric_distribution_chart/metric_distribution_chart.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
ScaleType,
2020
Settings,
2121
TooltipValueFormatter,
22-
TooltipValue,
2322
} from '@elastic/charts';
2423

2524
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
@@ -28,6 +27,7 @@ import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
2827
import { MetricDistributionChartTooltipHeader } from './metric_distribution_chart_tooltip_header';
2928
import { useUiChromeContext } from '../../../../../contexts/ui/use_ui_chrome_context';
3029
import { kibanaFieldFormat } from '../../../../../formatters/kibana_field_format';
30+
import { ChartTooltipValue } from '../../../../../components/chart_tooltip/chart_tooltip_service';
3131

3232
export interface MetricDistributionChartData {
3333
x: number;
@@ -60,7 +60,7 @@ export const MetricDistributionChart: FC<Props> = ({ width, height, chartData, f
6060
const themeName = IS_DARK_THEME ? darkTheme : lightTheme;
6161
const AREA_SERIES_COLOR = themeName.euiColorVis1;
6262

63-
const headerFormatter: TooltipValueFormatter = (tooltipData: TooltipValue) => {
63+
const headerFormatter: TooltipValueFormatter = (tooltipData: ChartTooltipValue) => {
6464
const xValue = tooltipData.value;
6565
const chartPoint: MetricDistributionChartData | undefined = chartData.find(
6666
data => data.x === xValue

0 commit comments

Comments
 (0)