fix(plugin-chart-echarts): tooltip overflow bug (#22218)

Co-authored-by: Ville Brofeldt <ville.brofeldt@apple.com>
This commit is contained in:
Ville Brofeldt 2022-11-24 14:11:01 +02:00 committed by GitHub
parent 3bc0865d90
commit 2e650eaebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 105 additions and 74 deletions

View File

@ -36,7 +36,7 @@ import {
TimeSeriesDatum, TimeSeriesDatum,
} from '../types'; } from '../types';
import { getDateFormatter, parseMetricValue } from '../utils'; import { getDateFormatter, parseMetricValue } from '../utils';
import { getDefaultPosition } from '../../utils/tooltip'; import { getDefaultTooltip } from '../../utils/tooltip';
import { Refs } from '../../types'; import { Refs } from '../../types';
const defaultNumberFormatter = getNumberFormatter(); const defaultNumberFormatter = getNumberFormatter();
@ -234,8 +234,7 @@ export default function transformProps(
bottom: 0, bottom: 0,
}, },
tooltip: { tooltip: {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
appendToBody: true,
show: !inContextMenu, show: !inContextMenu,
trigger: 'axis', trigger: 'axis',
formatter: renderTooltipFactory(formatTime, headerFormatter), formatter: renderTooltipFactory(formatTime, headerFormatter),

View File

@ -39,7 +39,7 @@ import { convertInteger } from '../utils/convertInteger';
import { defaultGrid, defaultYAxis } from '../defaults'; import { defaultGrid, defaultYAxis } from '../defaults';
import { getPadding } from '../Timeseries/transformers'; import { getPadding } from '../Timeseries/transformers';
import { OpacityEnum } from '../constants'; import { OpacityEnum } from '../constants';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types'; import { Refs } from '../types';
export default function transformProps( export default function transformProps(
@ -139,6 +139,7 @@ export default function transformProps(
type: 'scatter', type: 'scatter',
data: outlierDatum.map(val => [name, val]), data: outlierDatum.map(val => [name, val]),
tooltip: { tooltip: {
...getDefaultTooltip(refs),
formatter: (param: { data: [string, number] }) => { formatter: (param: { data: [string, number] }) => {
const [outlierName, stats] = param.data; const [outlierName, stats] = param.data;
const headline = groupbyLabels.length const headline = groupbyLabels.length
@ -197,6 +198,7 @@ export default function transformProps(
type: 'boxplot', type: 'boxplot',
data: transformedData, data: transformedData,
tooltip: { tooltip: {
...getDefaultTooltip(refs),
formatter: (param: CallbackDataParams) => { formatter: (param: CallbackDataParams) => {
// @ts-ignore // @ts-ignore
const { const {
@ -273,7 +275,7 @@ export default function transformProps(
nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end', nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
}, },
tooltip: { tooltip: {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
trigger: 'item', trigger: 'item',
axisPointer: { axisPointer: {

View File

@ -42,7 +42,7 @@ import {
} from '../utils/series'; } from '../utils/series';
import { defaultGrid } from '../defaults'; import { defaultGrid } from '../defaults';
import { OpacityEnum, DEFAULT_LEGEND_FORM_DATA } from '../constants'; import { OpacityEnum, DEFAULT_LEGEND_FORM_DATA } from '../constants';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types'; import { Refs } from '../types';
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
@ -215,7 +215,7 @@ export default function transformProps(
...defaultGrid, ...defaultGrid,
}, },
tooltip: { tooltip: {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
trigger: 'item', trigger: 'item',
formatter: (params: any) => formatter: (params: any) =>

View File

@ -44,7 +44,7 @@ import {
FONT_SIZE_MULTIPLIERS, FONT_SIZE_MULTIPLIERS,
} from './constants'; } from './constants';
import { OpacityEnum } from '../constants'; import { OpacityEnum } from '../constants';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types'; import { Refs } from '../types';
const setIntervalBoundsAndColors = ( const setIntervalBoundsAndColors = (
@ -261,7 +261,7 @@ export default function transformProps(
color: gaugeSeriesOptions.detail?.color, color: gaugeSeriesOptions.detail?.color,
}; };
const tooltip = { const tooltip = {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
formatter: (params: CallbackDataParams) => { formatter: (params: CallbackDataParams) => {
const { name, value } = params; const { name, value } = params;
return `${name} : ${formatValue(value as number)}`; return `${name} : ${formatValue(value as number)}`;
@ -315,7 +315,7 @@ export default function transformProps(
const echartOptions: EChartsCoreOption = { const echartOptions: EChartsCoreOption = {
tooltip: { tooltip: {
appendToBody: true, ...getDefaultTooltip(refs),
trigger: 'item', trigger: 'item',
}, },
series, series,

View File

@ -35,7 +35,7 @@ import {
} from './types'; } from './types';
import { DEFAULT_GRAPH_SERIES_OPTION } from './constants'; import { DEFAULT_GRAPH_SERIES_OPTION } from './constants';
import { getChartPadding, getLegendProps, sanitizeHtml } from '../utils/series'; import { getChartPadding, getLegendProps, sanitizeHtml } from '../utils/series';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types'; import { Refs } from '../types';
type EdgeWithStyles = GraphEdgeItemOption & { type EdgeWithStyles = GraphEdgeItemOption & {
@ -192,6 +192,7 @@ export default function transformProps(
sliceId, sliceId,
}: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData }; }: EchartsGraphFormData = { ...DEFAULT_GRAPH_FORM_DATA, ...formData };
const refs: Refs = {};
const metricLabel = getMetricLabel(metric); const metricLabel = getMetricLabel(metric);
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string); const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
const nodes: { [name: string]: number } = {}; const nodes: { [name: string]: number } = {};
@ -212,7 +213,10 @@ export default function transformProps(
value: 0, value: 0,
category, category,
select: DEFAULT_GRAPH_SERIES_OPTION.select, select: DEFAULT_GRAPH_SERIES_OPTION.select,
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip, tooltip: {
...getDefaultTooltip(refs),
...DEFAULT_GRAPH_SERIES_OPTION.tooltip,
},
}); });
} }
const node = echartNodes[nodes[name]]; const node = echartNodes[nodes[name]];
@ -296,13 +300,12 @@ export default function transformProps(
}, },
]; ];
const refs: Refs = {};
const echartOptions: EChartsCoreOption = { const echartOptions: EChartsCoreOption = {
animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration, animationDuration: DEFAULT_GRAPH_SERIES_OPTION.animationDuration,
animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing, animationEasing: DEFAULT_GRAPH_SERIES_OPTION.animationEasing,
tooltip: { tooltip: {
...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
position: getDefaultPosition(refs),
formatter: (params: any): string => formatter: (params: any): string =>
edgeFormatter( edgeFormatter(
params.data.source, params.data.source,

View File

@ -80,7 +80,7 @@ import {
transformTimeseriesAnnotation, transformTimeseriesAnnotation,
} from '../Timeseries/transformers'; } from '../Timeseries/transformers';
import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants'; import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
export default function transformProps( export default function transformProps(
chartProps: EchartsMixedTimeseriesProps, chartProps: EchartsMixedTimeseriesProps,
@ -425,9 +425,8 @@ export default function transformProps(
}, },
], ],
tooltip: { tooltip: {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
appendToBody: true,
trigger: richTooltip ? 'axis' : 'item', trigger: richTooltip ? 'axis' : 'item',
formatter: (params: any) => { formatter: (params: any) => {
const xValue: number = richTooltip const xValue: number = richTooltip

View File

@ -45,7 +45,7 @@ import {
} from '../utils/series'; } from '../utils/series';
import { defaultGrid } from '../defaults'; import { defaultGrid } from '../defaults';
import { convertInteger } from '../utils/convertInteger'; import { convertInteger } from '../utils/convertInteger';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types'; import { Refs } from '../types';
const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT);
@ -303,8 +303,8 @@ export default function transformProps(
...defaultGrid, ...defaultGrid,
}, },
tooltip: { tooltip: {
...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
position: getDefaultPosition(refs),
trigger: 'item', trigger: 'item',
formatter: (params: any) => formatter: (params: any) =>
formatPieLabel({ formatPieLabel({

View File

@ -44,7 +44,7 @@ import {
} from '../utils/series'; } from '../utils/series';
import { defaultGrid } from '../defaults'; import { defaultGrid } from '../defaults';
import { Refs } from '../types'; import { Refs } from '../types';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
export function formatLabel({ export function formatLabel({
params, params,
@ -232,7 +232,7 @@ export default function transformProps(
...defaultGrid, ...defaultGrid,
}, },
tooltip: { tooltip: {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
trigger: 'item', trigger: 'item',
}, },

View File

@ -84,7 +84,7 @@ import {
TIMESERIES_CONSTANTS, TIMESERIES_CONSTANTS,
TIMEGRAIN_TO_TIMESTAMP, TIMEGRAIN_TO_TIMESTAMP,
} from '../constants'; } from '../constants';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
export default function transformProps( export default function transformProps(
chartProps: EchartsTimeseriesChartProps, chartProps: EchartsTimeseriesChartProps,
@ -381,9 +381,8 @@ export default function transformProps(
xAxis, xAxis,
yAxis, yAxis,
tooltip: { tooltip: {
...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
position: getDefaultPosition(refs),
appendToBody: true,
trigger: richTooltip ? 'axis' : 'item', trigger: richTooltip ? 'axis' : 'item',
formatter: (params: any) => { formatter: (params: any) => {
const [xIndex, yIndex] = isHorizontal ? [1, 0] : [0, 1]; const [xIndex, yIndex] = isHorizontal ? [1, 0] : [0, 1];

View File

@ -31,6 +31,7 @@ import {
} from './types'; } from './types';
import { DEFAULT_FORM_DATA, DEFAULT_TREE_SERIES_OPTION } from './constants'; import { DEFAULT_FORM_DATA, DEFAULT_TREE_SERIES_OPTION } from './constants';
import { Refs } from '../types'; import { Refs } from '../types';
import { getDefaultTooltip } from '../utils/tooltip';
export function formatTooltip({ export function formatTooltip({
params, params,
@ -205,6 +206,7 @@ export default function transformProps(
animationEasing: DEFAULT_TREE_SERIES_OPTION.animationEasing, animationEasing: DEFAULT_TREE_SERIES_OPTION.animationEasing,
series, series,
tooltip: { tooltip: {
...getDefaultTooltip(refs),
trigger: 'item', trigger: 'item',
triggerOn: 'mousemove', triggerOn: 'mousemove',
formatter: (params: any) => formatter: (params: any) =>

View File

@ -47,7 +47,7 @@ import {
BORDER_COLOR, BORDER_COLOR,
} from './constants'; } from './constants';
import { OpacityEnum } from '../constants'; import { OpacityEnum } from '../constants';
import { getDefaultPosition } from '../utils/tooltip'; import { getDefaultTooltip } from '../utils/tooltip';
import { Refs } from '../types'; import { Refs } from '../types';
export function formatLabel({ export function formatLabel({
@ -310,7 +310,7 @@ export default function transformProps(
const echartOptions: EChartsCoreOption = { const echartOptions: EChartsCoreOption = {
tooltip: { tooltip: {
position: getDefaultPosition(refs), ...getDefaultTooltip(refs),
show: !inContextMenu, show: !inContextMenu,
trigger: 'item', trigger: 'item',
formatter: (params: any) => formatter: (params: any) =>

View File

@ -21,56 +21,59 @@ import { CallbackDataParams } from 'echarts/types/src/util/types';
import { TOOLTIP_OVERFLOW_MARGIN, TOOLTIP_POINTER_MARGIN } from '../constants'; import { TOOLTIP_OVERFLOW_MARGIN, TOOLTIP_POINTER_MARGIN } from '../constants';
import { Refs } from '../types'; import { Refs } from '../types';
export function getDefaultPosition(refs: Refs) { export function getDefaultTooltip(refs: Refs) {
return ( return {
canvasMousePos: [number, number], appendToBody: true,
params: CallbackDataParams, position: (
tooltipDom: HTMLDivElement | null, canvasMousePos: [number, number],
rect: any, params: CallbackDataParams,
sizes: { contentSize: [number, number]; viewSize: [number, number] }, tooltipDom: HTMLDivElement | null,
) => { rect: any,
// algorithm partially based on this snippet: sizes: { contentSize: [number, number]; viewSize: [number, number] },
// https://github.com/apache/echarts/issues/5004#issuecomment-559668309 ) => {
// algorithm partially based on this snippet:
// https://github.com/apache/echarts/issues/5004#issuecomment-559668309
// The chart canvas position // The chart canvas position
const divRect = refs.divRef?.current?.getBoundingClientRect(); const divRect = refs.divRef?.current?.getBoundingClientRect();
// The mouse coordinates relative to the whole window // The mouse coordinates relative to the whole window
// The first parameter to the position function is the mouse position relative to the canvas // The first parameter to the position function is the mouse position relative to the canvas
const mouseX = canvasMousePos[0] + (divRect?.x || 0); const mouseX = canvasMousePos[0] + (divRect?.x || 0);
const mouseY = canvasMousePos[1] + (divRect?.y || 0); const mouseY = canvasMousePos[1] + (divRect?.y || 0);
// The width and height of the tooltip dom element // The width and height of the tooltip dom element
const tooltipWidth = sizes.contentSize[0]; const tooltipWidth = sizes.contentSize[0];
const tooltipHeight = sizes.contentSize[1]; const tooltipHeight = sizes.contentSize[1];
// Start by placing the tooltip top and right relative to the mouse position // Start by placing the tooltip top and right relative to the mouse position
let xPos = mouseX + TOOLTIP_POINTER_MARGIN; let xPos = mouseX + TOOLTIP_POINTER_MARGIN;
let yPos = mouseY - TOOLTIP_POINTER_MARGIN - tooltipHeight; let yPos = mouseY - TOOLTIP_POINTER_MARGIN - tooltipHeight;
// The tooltip is overflowing past the right edge of the window // The tooltip is overflowing past the right edge of the window
if (xPos + tooltipWidth >= document.documentElement.clientWidth) { if (xPos + tooltipWidth >= document.documentElement.clientWidth) {
// Attempt to place the tooltip to the left of the mouse position // Attempt to place the tooltip to the left of the mouse position
xPos = mouseX - TOOLTIP_POINTER_MARGIN - tooltipWidth; xPos = mouseX - TOOLTIP_POINTER_MARGIN - tooltipWidth;
// The tooltip is overflowing past the left edge of the window // The tooltip is overflowing past the left edge of the window
if (xPos <= 0) if (xPos <= 0)
// Place the tooltip a fixed distance from the left edge of the window // Place the tooltip a fixed distance from the left edge of the window
xPos = TOOLTIP_OVERFLOW_MARGIN; xPos = TOOLTIP_OVERFLOW_MARGIN;
} }
// The tooltip is overflowing past the top edge of the window // The tooltip is overflowing past the top edge of the window
if (yPos <= 0) { if (yPos <= 0) {
// Attempt to place the tooltip to the bottom of the mouse position // Attempt to place the tooltip to the bottom of the mouse position
yPos = mouseY + TOOLTIP_POINTER_MARGIN; yPos = mouseY + TOOLTIP_POINTER_MARGIN;
// The tooltip is overflowing past the bottom edge of the window // The tooltip is overflowing past the bottom edge of the window
if (yPos + tooltipHeight >= document.documentElement.clientHeight) if (yPos + tooltipHeight >= document.documentElement.clientHeight)
// Place the tooltip a fixed distance from the top edge of the window // Place the tooltip a fixed distance from the top edge of the window
yPos = TOOLTIP_OVERFLOW_MARGIN; yPos = TOOLTIP_OVERFLOW_MARGIN;
} }
// Return the position (converted back to a relative position on the canvas) // Return the position (converted back to a relative position on the canvas)
return [xPos - (divRect?.x || 0), yPos - (divRect?.y || 0)]; return [xPos - (divRect?.x || 0), yPos - (divRect?.y || 0)];
},
}; };
} }

View File

@ -80,7 +80,11 @@ describe('EchartsGraph transformProps', () => {
label: { fontWeight: 'bolder' }, label: { fontWeight: 'bolder' },
}, },
symbolSize: 50, symbolSize: 50,
tooltip: { formatter: '{b}: {c}' }, tooltip: {
appendToBody: true,
formatter: '{b}: {c}',
position: expect.anything(),
},
value: 6, value: 6,
}, },
{ {
@ -93,7 +97,11 @@ describe('EchartsGraph transformProps', () => {
label: { fontWeight: 'bolder' }, label: { fontWeight: 'bolder' },
}, },
symbolSize: 50, symbolSize: 50,
tooltip: { formatter: '{b}: {c}' }, tooltip: {
appendToBody: true,
formatter: '{b}: {c}',
position: expect.anything(),
},
value: 6, value: 6,
}, },
{ {
@ -106,7 +114,11 @@ describe('EchartsGraph transformProps', () => {
label: { fontWeight: 'bolder' }, label: { fontWeight: 'bolder' },
}, },
symbolSize: 10, symbolSize: 10,
tooltip: { formatter: '{b}: {c}' }, tooltip: {
appendToBody: true,
formatter: '{b}: {c}',
position: expect.anything(),
},
value: 5, value: 5,
}, },
{ {
@ -119,7 +131,11 @@ describe('EchartsGraph transformProps', () => {
label: { fontWeight: 'bolder' }, label: { fontWeight: 'bolder' },
}, },
symbolSize: 10, symbolSize: 10,
tooltip: { formatter: '{b}: {c}' }, tooltip: {
appendToBody: true,
formatter: '{b}: {c}',
position: expect.anything(),
},
value: 5, value: 5,
}, },
], ],
@ -218,7 +234,11 @@ describe('EchartsGraph transformProps', () => {
symbolSize: 10, symbolSize: 10,
category: 'category_value_1', category: 'category_value_1',
select: DEFAULT_GRAPH_SERIES_OPTION.select, select: DEFAULT_GRAPH_SERIES_OPTION.select,
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip, tooltip: {
appendToBody: true,
formatter: '{b}: {c}',
position: expect.anything(),
},
label: { show: true }, label: { show: true },
}, },
{ {
@ -228,7 +248,11 @@ describe('EchartsGraph transformProps', () => {
symbolSize: 10, symbolSize: 10,
category: 'category_value_2', category: 'category_value_2',
select: DEFAULT_GRAPH_SERIES_OPTION.select, select: DEFAULT_GRAPH_SERIES_OPTION.select,
tooltip: DEFAULT_GRAPH_SERIES_OPTION.tooltip, tooltip: {
appendToBody: true,
formatter: '{b}: {c}',
position: expect.anything(),
},
label: { show: true }, label: { show: true },
}, },
], ],