Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions draftlogs/8145_add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add TypeScript declarations for lifecycle and WebGL context events [[#8145](https://github.com/plotly/plotly.js/pull/8145)]
11 changes: 10 additions & 1 deletion lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,28 +81,37 @@ export type {

export type {
BeforePlotEvent,
ButtonClickedEvent,
ClickAnnotationEvent,
FrameAnimationEvent,
IcicleClickEvent,
IciclePlotDatum,
LegendClickEvent,
LegendTitleClickEvent,
PlotCoordinate,
PlotDatum,
PlotHoverEvent,
PlotlyEventName,
PlotlyHTMLElement,
PlotMouseEvent,
PlotReactEvent,
PlotRelayoutEvent,
PlotRestyleEvent,
PlotRestyleEventUpdate,
PlotScatterDataPoint,
PlotScene,
PlotSelectedData,
PlotSelectionEvent,
PlotUpdateEvent,
SelectionRange,
SliderChangeEvent,
SliderEndEvent,
SliderStartEvent,
SunburstClickEvent,
SunburstPlotDatum
SunburstPlotDatum,
TreemapClickEvent,
TreemapPlotDatum,
WebGLContextLostEvent
} from '../src/types/core/events';

// ---------------------------------------------------------------------------
Expand Down
112 changes: 108 additions & 4 deletions src/types/core/events.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
LayoutAxis,
Slider,
Transition,
UpdateMenu
} from '../generated/schema';
import type { Datum } from '../lib/common';
import type { Config } from './config';
Expand Down Expand Up @@ -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<string, any>;
}

/** Payload for `plotly_hover` — augments `PlotMouseEvent` with axis values. */
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -235,6 +246,45 @@ export interface LegendClickEvent {
fullLayout: Partial<Layout>;
}

/**
* 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<Layout>;
/** Fully-resolved data array (defaults applied). */
fullData: Data[];
/** Fully-resolved layout object (defaults applied). */
fullLayout: Partial<Layout>;
}

/** Payload for `plotly_react`, emitted after a `Plotly.react` call that left the config unchanged. */
export interface PlotReactEvent {
/** User-supplied config object. */
config: Partial<Config>;
/** User-supplied data array. */
data: Data[];
/** User-supplied layout object. */
layout: Partial<Layout>;
}
Comment on lines +268 to +276

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.

Small mismatch here vs what the runtime actually does:

config is not optional here, but a call without a config sends config: undefined.

gd.on('plotly_react', (e) => console.log(e.config.responsive)); // no TS error
Plotly.react(gd, data, layout);
// TypeError: Cannot read properties of undefined (reading 'responsive')


/** 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<UpdateMenu['buttons']>[number];
/** Index of the active button, or `-1` if none is active. */
active: number;
}

// ---------------------------------------------------------------------------
// Slider events
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Comment on lines +326 to +328

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.

This isn't new to this PR, but I just noticed it:

npm run test-types will not catch a broken type reference inside the declarations!

Example:

 export interface WebGLContextLostEvent {
     /** The original DOM `webglcontextlost` event. */
-    event: WebGLContextEvent;
+    event: WebGLContextEvnt;
  • npx tsc --noEmit -p test/types --skipLibCheck false fails as expected
  • npm run test-types passes

It's not obvious, but test/types/tsconfig.json is extending the root tsconfig, which sets skipLibCheck: true.

/** 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;
Expand Down Expand Up @@ -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;
Comment on lines +372 to +375

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.

These types reuse SunburstPlotDatum which declares that value, id, color, and hovertext are all required.
However, at runtime a click payload does NOT always have those fields:

gd.on('plotly_treemapclick', (e) => {
    const pt = e.points[0];
    panel.textContent = `${pt.label}: ${pt.value.toLocaleString()} people`; // TypeError: pt.value is undefined
    loadDetails(pt.id); // undefined without `ids`
});

Also, the sunburst event has the same gap today, and this PR extends the gap to two more events.
In this PR, nextLevel became optional for the same reason.


/** 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.
Expand Down Expand Up @@ -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;
Comment on lines +437 to +440

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.

An event handler that only observes the event (i.e. does NOT return false) reports a type error:

gd.on('plotly_legendtitleclick', (e) => {
    console.log({ legend: e.legendId });
});
error TS2769: No overload matches this call.
  The last overload gave the following error.
    Argument of type '"plotly_legendtitleclick"' is not assignable to parameter of type '"plotly_afterexport" | ...

/** 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. */
Expand All @@ -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. */
Expand All @@ -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
Expand All @@ -401,6 +497,7 @@ export type PlotlyEventName =
| 'plotly_afterexport'
| 'plotly_afterplot'
| 'plotly_animated'
| 'plotly_animating'
| 'plotly_animatingframe'
| 'plotly_animationinterrupted'
| 'plotly_autosize'
Expand All @@ -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'
Expand All @@ -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';
65 changes: 65 additions & 0 deletions test/types/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Consumer-side checks for the event payload types and the `on()` overloads.
*
* Nothing here runs. See `update_api.ts`.
*/

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
const numberRangeStart: number | undefined = event['xaxis.range[0]'];
const dateRangeStart: string | undefined = event['xaxis.range[0]'];

// Any other attribute path is readable
const secondAxisStart = event['xaxis2.range[0]'];
const shapeStart = event['shapes[0].x0'];

// Layout properties are still typed
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<string, any> = 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', () => {});
23 changes: 0 additions & 23 deletions test/types/relayout_event.ts

This file was deleted.

Loading