Skip to content

Feature Request: Plotly.Fx.hover() should support unified hover mode with categorical x-axes #7682

@timdonohue-aerology

Description

@timdonohue-aerology

Use Case

I'm building a Dash app with synchronized hover between an AG Grid table and a Plotly chart. When a user hovers over a table row, I want to trigger the chart's hover tooltip at the corresponding time point to show all trace values (runway configuration probabilities over time).

The chart uses layout.hovermode = "x unified" to show all traces together in one tooltip when hovering naturally with the mouse.

Current Behavior & Limitations

Attempt 1: Point-based hover

// Shows only a single trace, not unified
Plotly.Fx.hover(plotElement, [{curveNumber: 0, pointNumber: 5}]);

Result: Shows tooltip for only trace 0, ignores hovermode: "x unified"

Attempt 2: X-value based hover

// Fails with categorical x-axis (time strings like "01/03 12:00")
Plotly.Fx.hover(plotElement, {xval: "01/03 12:00"}, ['xy']);

Result: Logs Fx.hover failed - doesn't work with categorical/string x-axis values

Attempt 3: Multiple points

// Shows 5 separate tooltips instead of unified
const points = [];
for (let i = 0; i < plotElement.data.length; i++) {
    points.push({curveNumber: i, pointNumber: 5});
}
Plotly.Fx.hover(plotElement, points);

Result: Shows individual tooltip per trace (not unified)

Current Workaround

I'm using synthetic MouseEvent dispatch with pixel coordinate calculation:

// Calculate pixel position using internal APIs
const layout = plotElement._fullLayout;  // ⚠️ Internal API
const xaxis = layout.xaxis;
const yaxis = layout.yaxis;

const xPixel = xaxis.l2p(rowIndex) + xaxis._offset;  // ⚠️ l2p() is internal
const yPixel = yaxis._offset + yaxis._length / 2;

// Dispatch synthetic mouse event
const plotArea = plotElement.querySelector('.nsewdrag');  // ⚠️ Internal class
const rect = plotElement.getBoundingClientRect();
plotArea.dispatchEvent(new MouseEvent('mousemove', {
    bubbles: true,
    cancelable: true,
    clientX: rect.left + xPixel,
    clientY: rect.top + yPixel,
    view: window
}));

This works but is fragile because it relies on:

  • _fullLayout (internal object)
  • xaxis.l2p() (internal method)
  • xaxis._offset, yaxis._offset, yaxis._length (internal properties)
  • .nsewdrag (internal CSS class)

These could break on Plotly.js upgrades.

Proposed Solution

Enhance Plotly.Fx.hover() to respect layout.hovermode when triggered programmatically:

Option 1: Auto-detect from layout

// When layout.hovermode = "x unified", show unified tooltip
Plotly.Fx.hover(plotElement, {xval: "01/03 12:00"});

Option 2: Explicit unified flag

Plotly.Fx.hover(plotElement, {xval: "01/03 12:00"}, {unified: true});

Option 3: Support categorical x-values with point index

// Use point index for categorical axes, auto-triggers unified mode
Plotly.Fx.hover(plotElement, {pointIndex: 5, unified: true});

Benefits

  • ✅ Uses public API instead of internal DOM/pixel manipulation
  • ✅ Won't break on Plotly.js version upgrades
  • ✅ Enables common use cases: table/chart sync, timeline controls, external hover triggers
  • ✅ Respects existing hovermode configuration
  • ✅ More intuitive API (matches how manual hover works)

Environment

  • Plotly.js version: 6.5.0 (via Dash 3.3.0)
  • Browser: Chrome/Safari (reproduced in both)
  • Chart type: Area chart with categorical x-axis (time strings)
  • Hovermode: "x unified"

Willingness to Contribute

I'm willing to submit a PR for this if the approach is acceptable to maintainers. Please advise on preferred API design before I start implementation.

Related Use Cases

This would benefit anyone needing programmatic hover control:

  • Synchronized charts and data tables
  • Timeline scrubbing controls
  • External search/filter highlighting
  • Keyboard navigation of data points
  • Accessibility features (screen reader integration)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions