You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/06/13 23:53:35 UTC
(superset) branch master updated: fix(echarts): cap tooltip at 80vh with internal scroll (#37022)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new c0fbe243e8b fix(echarts): cap tooltip at 80vh with internal scroll (#37022)
c0fbe243e8b is described below
commit c0fbe243e8bd8dd1efbc003194e1f16c9ffaa605
Author: Vanessa Giannoni <38...@users.noreply.github.com>
AuthorDate: Sat Jun 13 20:53:28 2026 -0300
fix(echarts): cap tooltip at 80vh with internal scroll (#37022)
Co-authored-by: Claude Code <no...@anthropic.com>
---
.../src/utils/tooltip.test.tsx | 87 ++++++++++++++++++++++
.../plugin-chart-echarts/src/utils/tooltip.ts | 75 ++++++++++++++-----
2 files changed, 144 insertions(+), 18 deletions(-)
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.test.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.test.tsx
new file mode 100644
index 00000000000..b6601c11cd3
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.test.tsx
@@ -0,0 +1,87 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.
+ */
+
+/** @jest-environment jsdom */
+import '@testing-library/jest-dom';
+import React from 'react';
+import { getDefaultTooltip } from './tooltip';
+import type { Refs } from '../types';
+
+test('getDefaultTooltip computes height and sets scroll styles', () => {
+ const div = document.createElement('div');
+ jest.spyOn(div, 'getBoundingClientRect').mockReturnValue({
+ x: 0,
+ y: 0,
+ width: 100,
+ height: 100,
+ top: 0,
+ right: 100,
+ bottom: 100,
+ left: 0,
+ toJSON: () => ({}),
+ } as DOMRect);
+
+ const divRef = React.createRef<HTMLDivElement>();
+ Object.defineProperty(divRef, 'current', { value: div });
+
+ const refs: Refs = { divRef };
+
+ // Set viewport dimensions
+ Object.defineProperty(document.documentElement, 'clientWidth', {
+ value: 1200,
+ configurable: true,
+ });
+ Object.defineProperty(document.documentElement, 'clientHeight', {
+ value: 1000,
+ configurable: true,
+ });
+
+ const tooltip = getDefaultTooltip(refs);
+
+ // Test behavioral properties
+ expect(tooltip.enterable).toBe(true);
+ expect(tooltip.confine).toBe(true);
+ expect(tooltip.hideDelay).toBe(50);
+
+ // Test that position function exists and is callable
+ expect(typeof tooltip.position).toBe('function');
+
+ // Test position function behavior - returns coordinates
+ const tooltipDom = document.createElement('div');
+ const result = tooltip.position(
+ [200, 300],
+ {} as any,
+ tooltipDom,
+ {} as any,
+ { contentSize: [300, 600], viewSize: [1200, 1000] },
+ );
+
+ // Verify the actual computed position, not just its shape. The cursor sits
+ // in the right half of the chart, so the tooltip is placed to its left;
+ // that pushes it past the left edge, so it clamps to the overflow margin.
+ // Vertically it would overflow the top, so it flips to just below the cursor.
+ expect(result).toEqual([5, 310]);
+
+ const computedMaxHeight = parseInt(tooltipDom.style.maxHeight, 10);
+ const viewportHeight = 1000;
+ const expectedMaxHeight = Math.min(800, viewportHeight * 0.8);
+
+ expect(computedMaxHeight).toBe(expectedMaxHeight);
+ expect(tooltipDom).toHaveStyle({ overflow: 'auto' });
+});
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts
index c940f5fea0c..59aaf63b0d7 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/tooltip.ts
@@ -34,6 +34,12 @@ export function getDefaultTooltip(refs: Refs) {
borderColor: 'transparent',
// CSS hack applied on this class to resolve https://github.com/apache/superset/issues/30058
className: 'echarts-tooltip',
+ // allow scrolling inside tooltip without re-triggering the chart
+ enterable: true,
+ // keep within viewport and above header; enable internal scroll
+ confine: true,
+ // optional: reduce flicker when moving in/out of tooltip
+ hideDelay: 50,
position: (
canvasMousePos: [number, number],
params: CallbackDataParams,
@@ -41,43 +47,76 @@ export function getDefaultTooltip(refs: Refs) {
rect: any,
sizes: { contentSize: [number, number]; viewSize: [number, number] },
) => {
- // algorithm partially based on this snippet:
+ // Algorithm partially based on this snippet:
// https://github.com/apache/echarts/issues/5004#issuecomment-559668309
- // The chart canvas position
const divRect = refs.divRef?.current?.getBoundingClientRect();
-
- // The mouse coordinates relative to the whole window
- // The first parameter to the position function is the mouse position relative to the canvas
const mouseX = canvasMousePos[0] + (divRect?.x || 0);
const mouseY = canvasMousePos[1] + (divRect?.y || 0);
-
- // The width and height of the tooltip dom element
+ const viewportWidth = document.documentElement.clientWidth;
+ const viewportHeight = document.documentElement.clientHeight;
const tooltipWidth = sizes.contentSize[0];
const tooltipHeight = sizes.contentSize[1];
- // Start by placing the tooltip top and right relative to the mouse position
- let xPos = mouseX + TOOLTIP_POINTER_MARGIN;
- let yPos = mouseY - TOOLTIP_POINTER_MARGIN - tooltipHeight;
+ // Cap tooltip height to reduce blocking adjacent elements
+ const maxAllowedHeight = Math.min(800, Math.floor(viewportHeight * 0.8));
+ const needsScrolling = tooltipHeight > maxAllowedHeight;
+
+ if (tooltipDom) {
+ tooltipDom.style.maxHeight = `${maxAllowedHeight}px`;
+ tooltipDom.style.overflow = 'auto';
+ // Only enable pointer events when tooltip is scrollable
+ // This prevents blocking adjacent chart elements when scrolling isn't needed
+ tooltipDom.style.pointerEvents = needsScrolling ? 'auto' : 'none';
+ }
+
+ const effectiveTooltipHeight = Math.min(tooltipHeight, maxAllowedHeight);
+ let xPos: number;
+ let yPos: number;
- // The tooltip is overflowing past the right edge of the window
- if (xPos + tooltipWidth >= document.documentElement.clientWidth) {
- // Attempt to place the tooltip to the left of the mouse position
- xPos = mouseX - TOOLTIP_POINTER_MARGIN - tooltipWidth;
+ // For scrollable tooltips, position further away horizontally to avoid blocking chart navigation
+ const horizontalMargin = needsScrolling
+ ? TOOLTIP_POINTER_MARGIN * 3
+ : TOOLTIP_POINTER_MARGIN;
- // The tooltip is overflowing past the left edge of the window
- if (xPos <= 0)
- // Place the tooltip a fixed distance from the left edge of the window
+ // Determine if cursor is in the right half of the chart to intelligently position tooltip
+ const chartWidth = divRect?.width || viewportWidth;
+ const cursorXInChart = canvasMousePos[0];
+ const isInRightHalfOfChart = cursorXInChart > chartWidth / 2;
+
+ // Position tooltip on the left when in right half, right when in left half
+ // This prevents blocking chart navigation
+ if (isInRightHalfOfChart) {
+ xPos = mouseX - horizontalMargin - tooltipWidth;
+
+ // If tooltip would go off left edge of viewport, push it back in
+ if (xPos <= 0) {
xPos = TOOLTIP_OVERFLOW_MARGIN;
+ }
+ } else {
+ xPos = mouseX + horizontalMargin;
+
+ // If tooltip would go off right edge of viewport, position on left instead
+ if (xPos + tooltipWidth >= viewportWidth) {
+ xPos = mouseX - horizontalMargin - tooltipWidth;
+
+ // If still overflowing left edge, clamp to margin
+ if (xPos <= 0) {
+ xPos = TOOLTIP_OVERFLOW_MARGIN;
+ }
+ }
}
+ // Position tooltip above cursor, or below if no space
+ yPos = mouseY - TOOLTIP_POINTER_MARGIN - effectiveTooltipHeight;
+
// The tooltip is overflowing past the top edge of the window
if (yPos <= 0) {
// Attempt to place the tooltip to the bottom of the mouse position
yPos = mouseY + TOOLTIP_POINTER_MARGIN;
// The tooltip is overflowing past the bottom edge of the window
- if (yPos + tooltipHeight >= document.documentElement.clientHeight)
+ if (yPos + effectiveTooltipHeight >= viewportHeight)
// Place the tooltip a fixed distance from the top edge of the window
yPos = TOOLTIP_OVERFLOW_MARGIN;
}