From bdac040ee759c75c5ddc5890a77dad382d20641b Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 23 Oct 2026 11:57:21 -0600 Subject: [PATCH 1/5] chore: Add missing types for events --- lib/index.d.ts | 11 +++- src/types/core/events.d.ts | 112 +++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 5 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index da892f0a9f8..f0fb53d9b79 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -81,15 +81,20 @@ export type { export type { BeforePlotEvent, + ButtonClickedEvent, ClickAnnotationEvent, FrameAnimationEvent, + IcicleClickEvent, + IciclePlotDatum, LegendClickEvent, + LegendTitleClickEvent, PlotCoordinate, PlotDatum, PlotHoverEvent, PlotlyEventName, PlotlyHTMLElement, PlotMouseEvent, + PlotReactEvent, PlotRelayoutEvent, PlotRestyleEvent, PlotRestyleEventUpdate, @@ -97,12 +102,16 @@ export type { PlotScene, PlotSelectedData, PlotSelectionEvent, + PlotUpdateEvent, SelectionRange, SliderChangeEvent, SliderEndEvent, SliderStartEvent, SunburstClickEvent, - SunburstPlotDatum + SunburstPlotDatum, + TreemapClickEvent, + TreemapPlotDatum, + WebGLContextLostEvent } from '../src/types/core/events'; // --------------------------------------------------------------------------- diff --git a/src/types/core/events.d.ts b/src/types/core/events.d.ts index 8023f0dba74..c6f128e63b1 100644 --- a/src/types/core/events.d.ts +++ b/src/types/core/events.d.ts @@ -14,6 +14,7 @@ import type { LayoutAxis, Slider, Transition, + UpdateMenu } from '../generated/schema'; import type { Datum } from '../lib/common'; import type { Config } from './config'; @@ -113,6 +114,8 @@ export interface PlotMouseEvent { xPixel?: number; /** Cursor y position in pixels from the graph div's top-left corner */ yPixel?: number; + /** Category constraints of the hovered or clicked path, set by `parcats` traces only. */ + constraints?: Record; } /** Payload for `plotly_hover` — augments `PlotMouseEvent` with axis values. */ @@ -157,6 +160,14 @@ export type PlotRestyleEvent = [PlotRestyleEventUpdate, number[]]; */ export type PlotRelayoutEvent = LayoutUpdate; +/** Payload for `plotly_update`: the restyle and relayout payloads of one `Plotly.update` call. */ +export interface PlotUpdateEvent { + /** Restyle part of the update: the attribute changes and the affected trace indices. */ + data: PlotRestyleEvent; + /** Relayout part of the update. */ + layout: PlotRelayoutEvent; +} + // --------------------------------------------------------------------------- // 3D scene / annotation / animation / legend events // --------------------------------------------------------------------------- @@ -235,6 +246,45 @@ export interface LegendClickEvent { fullLayout: Partial; } +/** + * Payload for `plotly_legendtitleclick` and `plotly_legendtitledoubleclick`. + * Return `false` from the handler to suppress Plotly's default title click behavior. + */ +export interface LegendTitleClickEvent { + /** The original DOM mouse event. */ + event: MouseEvent; + /** Id of the legend whose title was clicked, for example `'legend2'`. */ + legendId: string; + /** User-supplied data array. */ + data: Data[]; + /** User-supplied layout object. */ + layout: Partial; + /** Fully-resolved data array (defaults applied). */ + fullData: Data[]; + /** Fully-resolved layout object (defaults applied). */ + fullLayout: Partial; +} + +/** Payload for `plotly_react`, emitted after a `Plotly.react` call that left the config unchanged. */ +export interface PlotReactEvent { + /** User-supplied config object. */ + config: Partial; + /** User-supplied data array. */ + data: Data[]; + /** User-supplied layout object. */ + layout: Partial; +} + +/** Payload for `plotly_buttonclicked`, emitted when a button of an updatemenu is clicked. */ +export interface ButtonClickedEvent { + /** The updatemenu that holds the button. */ + menu: UpdateMenu; + /** The clicked button. */ + button: NonNullable[number]; + /** Index of the active button, or `-1` if none is active. */ + active: number; +} + // --------------------------------------------------------------------------- // Slider events // --------------------------------------------------------------------------- @@ -268,11 +318,23 @@ export interface SliderEndEvent { step: SliderStep; } +// --------------------------------------------------------------------------- +// WebGL context events +// --------------------------------------------------------------------------- + +/** Payload for `plotly_webglcontextlost`, emitted when a WebGL context is lost. */ +export interface WebGLContextLostEvent { + /** The original DOM `webglcontextlost` event. */ + event: WebGLContextEvent; + /** Identifier of the regl layer or the gl3d scene that lost its context. */ + layer: string; +} + // --------------------------------------------------------------------------- // Sunburst / before-plot // --------------------------------------------------------------------------- -/** Point payload for `plotly_sunburstclick`. */ +/** Point payload for `plotly_sunburstclick`. Treemap and icicle traces emit the same point shape. */ export interface SunburstPlotDatum { /** Color value associated with the sector. */ color: number; @@ -306,16 +368,28 @@ export interface SunburstPlotDatum { value: number; } -/** Payload for `plotly_sunburstclick`, including the next-level entry. */ +/** Point payload for `plotly_treemapclick`. Same shape as `SunburstPlotDatum`. */ +export type TreemapPlotDatum = SunburstPlotDatum; + +/** Point payload for `plotly_icicleclick`. Same shape as `SunburstPlotDatum`. */ +export type IciclePlotDatum = SunburstPlotDatum; + +/** Payload for `plotly_sunburstclick`. Treemap and icicle traces emit the same payload, including the next-level entry. */ export interface SunburstClickEvent { /** The original DOM mouse event. */ event: MouseEvent; - /** Sector id that becomes the entry on click. */ - nextLevel: string; + /** Sector id that becomes the entry on click. Absent when the click does not change the entry, as on a sunburst root or leaf. */ + nextLevel?: string; /** Clicked sector(s). */ points: SunburstPlotDatum[]; } +/** Payload for `plotly_treemapclick`. Same shape as `SunburstClickEvent`. */ +export type TreemapClickEvent = SunburstClickEvent; + +/** Payload for `plotly_icicleclick`. Same shape as `SunburstClickEvent`. */ +export type IcicleClickEvent = SunburstClickEvent; + /** * Payload for `plotly_beforeplot`. Returning `false` from the handler * cancels the plot operation. @@ -349,12 +423,27 @@ export interface PlotlyHTMLElement extends HTMLElement { on(event: 'plotly_restyle', callback: (data: PlotRestyleEvent) => void): void; /** Subscribe to relayout events (layout updates, including in-progress). */ on(event: 'plotly_relayout' | 'plotly_relayouting', callback: (event: PlotRelayoutEvent) => void): void; + /** Subscribe to update events (`Plotly.update` calls). */ + on(event: 'plotly_update', callback: (event: PlotUpdateEvent) => void): void; + /** Subscribe to WebGL context loss events. */ + on(event: 'plotly_webglcontextlost', callback: (event: WebGLContextLostEvent) => void): void; /** Subscribe to annotation click events. */ on(event: 'plotly_clickannotation', callback: (event: ClickAnnotationEvent) => void): void; /** Subscribe to per-frame animation events. */ on(event: 'plotly_animatingframe', callback: (event: FrameAnimationEvent) => void): void; /** Subscribe to legend click events; return `false` to suppress default toggle. */ on(event: 'plotly_legendclick' | 'plotly_legenddoubleclick', callback: (event: LegendClickEvent) => boolean): void; + /** Subscribe to legend title click events; return `false` to suppress default behavior. */ + on( + event: 'plotly_legendtitleclick' | 'plotly_legendtitledoubleclick', + callback: (event: LegendTitleClickEvent) => boolean + ): void; + /** Subscribe to react events. */ + on(event: 'plotly_react', callback: (event: PlotReactEvent) => void): void; + /** Subscribe to updatemenu button click events. */ + on(event: 'plotly_buttonclicked', callback: (event: ButtonClickedEvent) => void): void; + /** Subscribe to pre-hover events; return `false` to cancel the hover. */ + on(event: 'plotly_beforehover', callback: (event: MouseEvent) => boolean): void; /** Subscribe to slider change events. */ on(event: 'plotly_sliderchange', callback: (event: SliderChangeEvent) => void): void; /** Subscribe to slider end-of-interaction events. */ @@ -363,6 +452,10 @@ export interface PlotlyHTMLElement extends HTMLElement { on(event: 'plotly_sliderstart', callback: (event: SliderStartEvent) => void): void; /** Subscribe to sunburst sector click events. */ on(event: 'plotly_sunburstclick', callback: (event: SunburstClickEvent) => void): void; + /** Subscribe to treemap sector click events. */ + on(event: 'plotly_treemapclick', callback: (event: TreemapClickEvent) => void): void; + /** Subscribe to icicle sector click events. */ + on(event: 'plotly_icicleclick', callback: (event: IcicleClickEvent) => void): void; /** Subscribe to generic plotly events (low-level catch-all). */ on(event: 'plotly_event', callback: (data: any) => void): void; /** Subscribe to pre-plot events; return `false` to cancel plotting. */ @@ -373,13 +466,16 @@ export interface PlotlyHTMLElement extends HTMLElement { | 'plotly_afterexport' | 'plotly_afterplot' | 'plotly_animated' + | 'plotly_animating' | 'plotly_animationinterrupted' | 'plotly_autosize' | 'plotly_beforeexport' | 'plotly_deselect' | 'plotly_doubleclick' + | 'plotly_exportfail' | 'plotly_framework' | 'plotly_redraw' + | 'plotly_transitioned' | 'plotly_transitioning' | 'plotly_transitioninterrupted', callback: () => void @@ -401,6 +497,7 @@ export type PlotlyEventName = | 'plotly_afterexport' | 'plotly_afterplot' | 'plotly_animated' + | 'plotly_animating' | 'plotly_animatingframe' | 'plotly_animationinterrupted' | 'plotly_autosize' @@ -412,10 +509,15 @@ export type PlotlyEventName = | 'plotly_clickannotation' | 'plotly_deselect' | 'plotly_doubleclick' + | 'plotly_exportfail' | 'plotly_framework' | 'plotly_hover' + | 'plotly_icicleclick' | 'plotly_legendclick' | 'plotly_legenddoubleclick' + | 'plotly_legendtitleclick' + | 'plotly_legendtitledoubleclick' + | 'plotly_react' | 'plotly_redraw' | 'plotly_relayout' | 'plotly_relayouting' @@ -426,8 +528,10 @@ export type PlotlyEventName = | 'plotly_sliderend' | 'plotly_sliderstart' | 'plotly_sunburstclick' + | 'plotly_transitioned' | 'plotly_transitioning' | 'plotly_transitioninterrupted' | 'plotly_treemapclick' | 'plotly_unhover' + | 'plotly_update' | 'plotly_webglcontextlost'; From 4393e526cb7d25798f7629bfe12720fb54617bde Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 23 Oct 2026 12:14:28 -0600 Subject: [PATCH 2/5] Rename test file --- test/types/{relayout_event.ts => events.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/types/{relayout_event.ts => events.ts} (100%) diff --git a/test/types/relayout_event.ts b/test/types/events.ts similarity index 100% rename from test/types/relayout_event.ts rename to test/types/events.ts From 830f530f5ae2f5def06bed60115fb0fe5a2069ef Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 23 Oct 2026 12:14:38 -0600 Subject: [PATCH 3/5] Add more event type tests --- test/types/events.ts | 46 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/test/types/events.ts b/test/types/events.ts index 56d26a90e35..e14ba404306 100644 --- a/test/types/events.ts +++ b/test/types/events.ts @@ -1,11 +1,12 @@ /** - * Consumer-side checks for the `plotly_relayout` event payload. + * Consumer-side checks for the event payload types and the `on()` overloads. * * Nothing here runs. See `update_api.ts`. */ -import type { PlotRelayoutEvent } from '../../lib/index'; +import type { PlotlyHTMLElement, PlotRelayoutEvent, TreemapPlotDatum } from '../../lib/index'; +declare const gd: PlotlyHTMLElement; declare const event: PlotRelayoutEvent; // A range value can be a number or a date string @@ -21,3 +22,44 @@ const autosize: boolean | undefined = event.autosize; // @ts-expect-error unknown top-level key const typo = event.titl; + +gd.on('plotly_update', (event) => { + const attributes: Record = event.data[0]; + const traces: number[] = event.data[1]; + const autosizeInUpdate: boolean | undefined = event.layout.autosize; +}); + +gd.on('plotly_webglcontextlost', (event) => { + const domEvent: WebGLContextEvent = event.event; + const layer: string = event.layer; +}); + +gd.on('plotly_react', (event) => { + const dataLength: number = event.data.length; +}); + +gd.on('plotly_legendtitleclick', (event) => { + const legendId: string = event.legendId; + return false; +}); + +gd.on('plotly_buttonclicked', (event) => { + const active: number = event.active; +}); + +gd.on('plotly_beforehover', (event) => { + const x: number = event.clientX; + return true; +}); + +gd.on('plotly_icicleclick', (event) => { + const nextLevel: string | undefined = event.nextLevel; +}); + +gd.on('plotly_treemapclick', (event) => { + const points: TreemapPlotDatum[] = event.points; +}); + +gd.on('plotly_animating', () => {}); +gd.on('plotly_transitioned', () => {}); +gd.on('plotly_exportfail', () => {}); From 708ef2c29a28008af305499898506e7645d6a5fd Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Fri, 23 Oct 2026 12:33:16 -0600 Subject: [PATCH 4/5] Add draftlog --- draftlogs/8145_add.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/8145_add.md diff --git a/draftlogs/8145_add.md b/draftlogs/8145_add.md new file mode 100644 index 00000000000..d55a68b379b --- /dev/null +++ b/draftlogs/8145_add.md @@ -0,0 +1 @@ + - Add TypeScript declarations for lifecycle and WebGL context events [[#8145](https://github.com/plotly/plotly.js/pull/8145)] From e0b28da216a0b02166cd5df6d3db298f4ebfd577 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Mon, 26 Oct 2026 17:05:05 -0600 Subject: [PATCH 5/5] Address PR feedback --- src/types/core/events.d.ts | 56 +++++++++++++++++++++----------------- test/types/events.ts | 48 ++++++++++++++++++++++++++++++++ test/types/tsconfig.json | 3 ++ 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/src/types/core/events.d.ts b/src/types/core/events.d.ts index c6f128e63b1..fb42ba475ad 100644 --- a/src/types/core/events.d.ts +++ b/src/types/core/events.d.ts @@ -265,14 +265,17 @@ export interface LegendTitleClickEvent { fullLayout: Partial; } -/** Payload for `plotly_react`, emitted after a `Plotly.react` call that left the config unchanged. */ +/** + * Payload for `plotly_react`. The event does not fire if the call changed the config. + * The `config` and `layout` values are `undefined` if the `Plotly.react` call omitted them. + */ export interface PlotReactEvent { /** User-supplied config object. */ - config: Partial; + config: Partial | undefined; /** User-supplied data array. */ data: Data[]; /** User-supplied layout object. */ - layout: Partial; + layout: Partial | undefined; } /** Payload for `plotly_buttonclicked`, emitted when a button of an updatemenu is clicked. */ @@ -336,8 +339,8 @@ export interface WebGLContextLostEvent { /** Point payload for `plotly_sunburstclick`. Treemap and icicle traces emit the same point shape. */ export interface SunburstPlotDatum { - /** Color value associated with the sector. */ - color: number; + /** Color value associated with the sector. Present only if the trace sets `marker.colors`. */ + color?: number | undefined; /** Index of the trace producing this sector. */ curveNumber: number; /** The trace this sector belongs to. */ @@ -346,14 +349,14 @@ export interface SunburstPlotDatum { entry: string; /** Fully-resolved trace data. */ fullData: Data; - /** Hover text shown for this sector. */ - hovertext: string; - /** Hierarchical id of the sector. */ - id: string; - /** Display label of the sector. */ - label: string; - /** Parent sector id. */ - parent: string; + /** Hover text shown for this sector. Present only if the trace sets `hovertext`. */ + hovertext?: string | undefined; + /** Hierarchical id of the sector. Present only if the trace sets `ids`. */ + id?: string | undefined; + /** Display label of the sector. Present only if the trace sets `labels`. */ + label?: string | undefined; + /** Parent sector id. Absent on the root sector. */ + parent?: string | undefined; /** Percentage of the immediate entry sector this sector represents. */ percentEntry: number; /** Percentage of the parent sector this sector represents. */ @@ -364,8 +367,8 @@ export interface SunburstPlotDatum { pointNumber: number; /** Root sector id. */ root: string; - /** Numeric value of the sector. */ - value: number; + /** Numeric value of the sector. Present only if the trace sets `values`. */ + value?: number | undefined; } /** Point payload for `plotly_treemapclick`. Same shape as `SunburstPlotDatum`. */ @@ -432,34 +435,37 @@ export interface PlotlyHTMLElement extends HTMLElement { /** Subscribe to per-frame animation events. */ on(event: 'plotly_animatingframe', callback: (event: FrameAnimationEvent) => void): void; /** Subscribe to legend click events; return `false` to suppress default toggle. */ - on(event: 'plotly_legendclick' | 'plotly_legenddoubleclick', callback: (event: LegendClickEvent) => boolean): void; + on( + event: 'plotly_legendclick' | 'plotly_legenddoubleclick', + callback: (event: LegendClickEvent) => boolean | void + ): void; /** Subscribe to legend title click events; return `false` to suppress default behavior. */ on( event: 'plotly_legendtitleclick' | 'plotly_legendtitledoubleclick', - callback: (event: LegendTitleClickEvent) => boolean + callback: (event: LegendTitleClickEvent) => boolean | void ): void; /** Subscribe to react events. */ on(event: 'plotly_react', callback: (event: PlotReactEvent) => void): void; /** Subscribe to updatemenu button click events. */ on(event: 'plotly_buttonclicked', callback: (event: ButtonClickedEvent) => void): void; /** Subscribe to pre-hover events; return `false` to cancel the hover. */ - on(event: 'plotly_beforehover', callback: (event: MouseEvent) => boolean): void; + on(event: 'plotly_beforehover', callback: (event: MouseEvent) => boolean | void): void; /** Subscribe to slider change events. */ on(event: 'plotly_sliderchange', callback: (event: SliderChangeEvent) => void): void; /** Subscribe to slider end-of-interaction events. */ on(event: 'plotly_sliderend', callback: (event: SliderEndEvent) => void): void; /** Subscribe to slider start-of-interaction events. */ on(event: 'plotly_sliderstart', callback: (event: SliderStartEvent) => void): void; - /** Subscribe to sunburst sector click events. */ - on(event: 'plotly_sunburstclick', callback: (event: SunburstClickEvent) => void): void; - /** Subscribe to treemap sector click events. */ - on(event: 'plotly_treemapclick', callback: (event: TreemapClickEvent) => void): void; - /** Subscribe to icicle sector click events. */ - on(event: 'plotly_icicleclick', callback: (event: IcicleClickEvent) => void): void; + /** Subscribe to sunburst sector click events; return `false` to suppress the default zoom. */ + on(event: 'plotly_sunburstclick', callback: (event: SunburstClickEvent) => boolean | void): void; + /** Subscribe to treemap sector click events; return `false` to suppress the default zoom. */ + on(event: 'plotly_treemapclick', callback: (event: TreemapClickEvent) => boolean | void): void; + /** Subscribe to icicle sector click events; return `false` to suppress the default zoom. */ + on(event: 'plotly_icicleclick', callback: (event: IcicleClickEvent) => boolean | void): void; /** Subscribe to generic plotly events (low-level catch-all). */ on(event: 'plotly_event', callback: (data: any) => void): void; /** Subscribe to pre-plot events; return `false` to cancel plotting. */ - on(event: 'plotly_beforeplot', callback: (event: BeforePlotEvent) => boolean): void; + on(event: 'plotly_beforeplot', callback: (event: BeforePlotEvent) => boolean | void): void; /** Subscribe to bare lifecycle events that carry no payload. */ on( event: diff --git a/test/types/events.ts b/test/types/events.ts index e14ba404306..035cdda787c 100644 --- a/test/types/events.ts +++ b/test/types/events.ts @@ -36,6 +36,11 @@ gd.on('plotly_webglcontextlost', (event) => { gd.on('plotly_react', (event) => { const dataLength: number = event.data.length; + const responsive: boolean | undefined = event.config?.responsive; + const autosize: boolean | undefined = event.layout?.autosize; + + // @ts-expect-error `config` is `undefined` if the `Plotly.react` call omitted it + const unchecked: boolean | undefined = event.config.responsive; }); gd.on('plotly_legendtitleclick', (event) => { @@ -43,6 +48,45 @@ gd.on('plotly_legendtitleclick', (event) => { return false; }); +// A handler that only observes the event is valid +gd.on('plotly_legendclick', (event) => { + console.log(event.curveNumber); +}); + +gd.on('plotly_legenddoubleclick', (event) => { + console.log(event.curveNumber); +}); + +gd.on('plotly_beforeplot', (event) => { + console.log(event.data.length); +}); + +gd.on('plotly_sunburstclick', (event) => { + console.log(event.points.length); +}); + +gd.on('plotly_treemapclick', (event) => { + console.log(event.points.length); +}); + +gd.on('plotly_icicleclick', (event) => { + console.log(event.points.length); +}); + +// A handler can return `false` to cancel the default behavior +gd.on('plotly_legendclick', () => false); +gd.on('plotly_beforeplot', () => false); +gd.on('plotly_sunburstclick', () => false); +gd.on('plotly_treemapclick', () => false); + +gd.on('plotly_legendtitleclick', (event) => { + console.log(event.legendId); +}); + +gd.on('plotly_beforehover', (event) => { + console.log(event.clientX); +}); + gd.on('plotly_buttonclicked', (event) => { const active: number = event.active; }); @@ -58,6 +102,10 @@ gd.on('plotly_icicleclick', (event) => { gd.on('plotly_treemapclick', (event) => { const points: TreemapPlotDatum[] = event.points; + + // @ts-expect-error `value` is absent unless the trace sets `values` + const value: number = points[0].value; + const optionalValue: number | undefined = points[0].value; }); gd.on('plotly_animating', () => {}); diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json index dea162a5ca2..3e7b9bfce79 100644 --- a/test/types/tsconfig.json +++ b/test/types/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "../../tsconfig.json", + "compilerOptions": { + "skipLibCheck": false + }, "include": ["./**/*.ts"], "exclude": [] }