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;
       }