Skip to content

feat: DH-22030: dx event handler#1373

Open
jnumainville wants to merge 6 commits into
deephaven:mainfrom
jnumainville:22030_dx_event_plan
Open

feat: DH-22030: dx event handler#1373
jnumainville wants to merge 6 commits into
deephaven:mainfrom
jnumainville:22030_dx_event_plan

Conversation

@jnumainville

@jnumainville jnumainville commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Adds dx event handlers

@jnumainville jnumainville requested review from dsmmcken and mofojed June 22, 2026 20:56

`traceName` is the Plotly trace name, which for Deephaven charts is the `by` column value (or partition key) that identifies the trace. If the chart has no `by`/partitioning, `traceName` is the default Plotly trace name.

### Layout Events (`on_relayout`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these events fire for every data point that ticks in? Something to keep in mind.

@jnumainville jnumainville Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one should not fire for every data point. The events that would have been filtered out already.
We only update a couple things, such as title and legend title, in the layout after init. So besides that case, this mainly updates for user initiated adjustment such as zooming. This is why we freeze the layout too, so we can have the events such as zoom.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need to debounce when the user is zooming, will need to play with it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added debouncing note. The main spot is spamming the zoom button.

Comment thread plans/DH-22030-plotly-events.md Outdated


fig = dx.sunburst(
pop_by_continent, names="Continent", values="Pop", on_sunburst_click=handle_sunburst

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's kind of annoying that we need to do on_sunburst_click for the plot vs. just doing a on_click - we already know it's a sunburst chart, why not just do an on_click handler when calling from dx.sunburst?
I guess in dx.layer we could have multiple types of charts, so I imagine we need to have more information on the click event there; so do we have one on_click and add different metadata for the type of click, or do we force the on_click, on_sunburst_click when we're in a layer (we'll still need extra metadata to link it to which plot was clicked either way yeah?)

@jnumainville jnumainville Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some generated plotly.js code I was playing with and after some further research I was missing something here. The plotly_click event fires already, even for the hierarchical plots, it just wasn't wired.
The hierarchical plots have different events because they actually change the visualized data, which is what I thought the key difference was, but the actual reason they exist is if you return False from the callback you can prevent the drill down.
I'm thinking that instead of that, we expose a drill_down param that can be used to disable drill down. Downside is it's not as flexible (no conditional disabling several layers down for example) but this is a helpful and simple switch for the basic use case where you just want the data out of the level you click on.
Also, we are filtering out most of the trace data. The idea is you can use the curve number to look up the specific trace if you need to. This has made me think it's worth piping through the trace type too.

Comment thread plans/DH-22030-plotly-events.md Outdated


def handle_legend_click(event):
print(f"Legend clicked: {event['traceName']} (curve {event['curveNumber']})")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm do we want these events converted to snake_case instead of camelCase...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll do that


### `on_relayout` — Layout Change (Pan/Zoom)

**Justification:** Synchronize the visible time range across multiple charts (e.g. pan one chart, update all others to the same window) or filter tables to the visible range.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I accept this justification, as it was an event I questioned. Concerned it might fire more than expected.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment to Bender. This specific one should be safe.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a detailed implementation plan for DH-22030 to support server-side Python callbacks for Plotly Express (dx) chart interaction events, including payload schemas, API signatures, and a phased rollout across Python + JS.

Changes:

  • Specifies supported Plotly event → Python callback parameters (plus hierarchical drill_down behavior).
  • Documents event payload schemas and provides usage examples for each event type.
  • Outlines a phased implementation plan across DeephavenFigure, message handling, and frontend wiring (plus proposed test coverage).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plans/DH-22030-plotly-events.md
Comment thread plans/DH-22030-plotly-events.md
Comment thread plans/DH-22030-plotly-events.md

@mofojed mofojed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sold on the drill_down behaviour yet, and not sure the legend click disabling the default behaviour was mentioned at all.

Comment thread plans/DH-22030-plotly-events.md Outdated
Comment on lines +37 to +45
### `drill_down`

Hierarchical chart functions (`sunburst`, `treemap`, `icicle`) now accept an additional non-callback parameter, based on the functionality enabled by returning false from `plotly_sunburstclick` / `plotly_treemapclick` / `plotly_icicleclick` handlers.

```python
drill_down: bool = True
```

When `False`, clicking a segment does **not** trigger the default drill-down animation. The `on_click` callback still fires with the clicked point data. This is implemented on the JS side by returning `false` from the underlying `plotly_sunburstclick` / `plotly_treemapclick` / `plotly_icicleclick` handler and is a replacement for that functionality being directly exposed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm so we'll have two props for a callback, e.g. dx.sunburst(..., on_click=handle_click, drill_down=False)?
Note there seems to be similar functionality when a legend is clicked: https://plotly.com/javascript/plotlyjs-events/#legend-click-events
Trying to think of how to hack this in a little more elegantly... since I'm not big on having extra props pollute the top level.
A couple options I think worth exploring:

  1. If there's a callback, always return false for these, and then get the result from the callback - if the callback result is False, do nothing, otherwise trigger the default action (depends on if we can "trigger" those default actions)
  2. Wrap the callback with something that has options, e.g. dx.click_handler(handle_click, default_behavior=False). I don't particularly like that but it would at least group the props with the callback handler rather than polluting the top scope.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's more that on_click handles the data return and we replace on_sunburst_click (or at least the return value part of it) with the drill_down prop. They can be used independently and I don't like the idea of on_click having to possibly return a value because that would depend on chart type or alternatively using on_sunburst_click, which we agree is gross...
We could still do that with legend events too. Something legend_click_default: bool would determine the return value of the callback on the js side. If you just had legend_click_default = False but no callback the js side callback would just disable the clicking behavior.
Of course it's highly likely that most of the uses of drill_down=False would be coupled with the callback, but there could be the case of wanting to disable interactions. It looks like it's been asked for a few times on plotly's forum at least (example)
But, returning back to the point of not wanting extra props if we can avoid it, I'll have to ponder further. This also isn't flexible in terms of conditional anyways, a related concern.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to option 1


`traceName` is the Plotly trace name, which for Deephaven charts is the `by` column value (or partition key) that identifies the trace. If the chart has no `by`/partitioning, `traceName` is the default Plotly trace name.

### Layout Events (`on_relayout`)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might need to debounce when the user is zooming, will need to play with it.

@jnumainville jnumainville requested a review from mofojed June 30, 2026 19:28
mofojed
mofojed previously approved these changes Jun 30, 2026
@jnumainville jnumainville changed the title feat: DH-22030: dx event handlers plan feat: DH-22030: dx event handler Jul 9, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 35 changed files in this pull request and generated 11 comments.

Comment thread plugins/plotly-express/src/js/src/usePlotlyEventCallbacks.ts
Comment thread plugins/plotly-express/src/js/src/PlotlyExpressChart.tsx
Comment on lines +516 to +518
if (clickId == null || this.isPreventable(clickId)) {
return;
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +520 to +526
points: event.points.map(p => ({
x: p.x,
y: p.y,
trace_name: p.data.name,
trace_type: p.data.type,
curve_number: p.curveNumber,
})),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

curve_number: event.curveNumber,
modifiers: this.getModifiers(),
};
this.sendEventCallbackWithResponse(legendClickId, args).then(allowed => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

"on_legend_click",
"on_legend_double_click",
"on_click_annotation",
"on_web_gl_context_lost",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// Verify callback result was written to the result table
await openPanel(page, 'events_click_result', '.iris-grid');
const grid = page.locator('.iris-grid .iris-grid-column');
await expect(grid).toBeVisible();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread tests/express_events.spec.ts Outdated
Comment on lines +47 to +49
const traces = chart.locator('.plot-container .trace');
const count = await traces.count();
expect(count).toBeGreaterThan(0);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread tests/express_events.spec.ts Outdated
Comment on lines +62 to +65
const sunburstSlice = chart.locator('.sunburst .slice').first();
if (await sunburstSlice.isVisible()) {
await sunburstSlice.click();
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread tests/express_events.spec.ts Outdated
Comment on lines +99 to +100
// Verify callback result
await openPanel(page, 'events_select_result', '.iris-grid');

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return func(*args, **kwargs)


def wrap_callable(func: Callable) -> Callable:

@jnumainville jnumainville Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the function above are the only thing I really ripped directly from ui. Might be worth adding to utilities package? Not sure.

@jnumainville

jnumainville commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

This will need deephaven/web-client-ui#2718. There's a few things that have to be done in there unfortunately (especially the relayout event and the selection icons) and there are some things that are "correct" to do there (most other event handlers). So this won't be fully done and passing until then.

Comment on lines +483 to +486
// Defer wiring to a microtask so our listener registers after
// react-plotly.js attaches its own, which clobbers our sunburst handler.
// Sunburst could be handled directly, but treemap and icicle are not
// supported by react-plotly.js at all so they are all handled here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems kind of janky, though necessary. I would file an issue against the react-plotly.js repo about the missing event handlers and being unable to capture those clicks: https://github.com/plotly/react-plotly.js/issues
Maybe add a comment on plotly/react-plotly.js#331 about how you can workaround it as well.
Just for posterity.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_containerRef: RefObject<HTMLElement | null>,
_model: PlotlyExpressChartModel | null
): void {
// Event wiring is handled in the model's wireEventCallbacks method

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused, what are we expecting to do here?
Couldn't we register the event listeners here instead of from within the model?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was should have been removed in the current version but am going to restructure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants