From 6e66ccdbadf40d8ec870496d20b1c50d8b42e601 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 11:39:08 -0600 Subject: [PATCH 1/8] Move events and related functions into separate file --- src/events.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/factory.js | 55 ++--------------------------------------- 2 files changed, 68 insertions(+), 53 deletions(-) create mode 100644 src/events.js diff --git a/src/events.js b/src/events.js new file mode 100644 index 0000000..56774de --- /dev/null +++ b/src/events.js @@ -0,0 +1,66 @@ +// The single source of truth for the plotly.js events this wrapper forwards. +// +// The naming convention is: +// - events are attached as `'plotly_' + name.toLowerCase()` +// - react props are `'on' + name` +// +// `triggersUpdate` marks events that plotly.js emits *after* mutating the +// figure itself (drill-downs, zooms, restyles). For those the wrapper has to +// re-read the graph div and fire `onUpdate`. Keeping the flag on the same +// record as the name is deliberate: a new drill-down event cannot be added +// without deciding whether it updates the figure. +// +// The `on*` prop types in `factory.d.ts` are maintained by hand against this list +export const events = [ + {name: 'AfterExport'}, + {name: 'AfterPlot'}, + {name: 'Animated', triggersUpdate: true}, + {name: 'AnimatingFrame'}, + {name: 'AnimationInterrupted'}, + {name: 'AutoSize'}, + {name: 'BeforeExport'}, + {name: 'BeforeHover'}, + {name: 'ButtonClicked'}, + {name: 'Click'}, + {name: 'ClickAnnotation'}, + {name: 'ClickAnywhere'}, + {name: 'Deselect'}, + {name: 'DoubleClick', triggersUpdate: true}, + {name: 'Framework'}, + {name: 'Hover'}, + {name: 'HoverAnywhere'}, + {name: 'IcicleClick', triggersUpdate: true}, + {name: 'LegendClick'}, + {name: 'LegendDoubleClick'}, + {name: 'Relayout', triggersUpdate: true}, + {name: 'Relayouting', triggersUpdate: true}, + {name: 'Restyle', triggersUpdate: true}, + {name: 'Redraw', triggersUpdate: true}, + {name: 'Selected'}, + {name: 'Selecting'}, + {name: 'SliderChange'}, + {name: 'SliderEnd'}, + {name: 'SliderStart'}, + {name: 'SunburstClick', triggersUpdate: true}, + {name: 'Transitioning'}, + {name: 'TransitionInterrupted'}, + {name: 'TreemapClick', triggersUpdate: true}, + {name: 'Unhover'}, + {name: 'WebGlContextLost'}, +]; + +/** The plotly.js event name a given entry is attached as. */ +export function getPlotlyEventName(eventName) { + return 'plotly_' + eventName.toLowerCase(); +} + +/** The React prop name a given entry is read from. */ +export function getPropName(eventName) { + return 'on' + eventName; +} + +export const eventNames = events.map((event) => event.name); + +export const updateEvents = events + .filter((event) => event.triggersUpdate) + .map((event) => getPlotlyEventName(event.name)); diff --git a/src/factory.js b/src/factory.js index 99c4860..823a4b9 100644 --- a/src/factory.js +++ b/src/factory.js @@ -1,53 +1,6 @@ import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; -// The naming convention is: -// - events are attached as `'plotly_' + eventName.toLowerCase()` -// - react props are `'on' + eventName` -const eventNames = [ - 'AfterExport', - 'AfterPlot', - 'Animated', - 'AnimatingFrame', - 'AnimationInterrupted', - 'AutoSize', - 'BeforeExport', - 'BeforeHover', - 'ButtonClicked', - 'Click', - 'ClickAnnotation', - 'ClickAnywhere', - 'Deselect', - 'DoubleClick', - 'Framework', - 'Hover', - 'HoverAnywhere', - 'LegendClick', - 'LegendDoubleClick', - 'Relayout', - 'Relayouting', - 'Restyle', - 'Redraw', - 'Selected', - 'Selecting', - 'SliderChange', - 'SliderEnd', - 'SliderStart', - 'SunburstClick', - 'Transitioning', - 'TransitionInterrupted', - 'Unhover', - 'WebGlContextLost', -]; - -const updateEvents = [ - 'plotly_restyle', - 'plotly_redraw', - 'plotly_relayout', - 'plotly_relayouting', - 'plotly_doubleclick', - 'plotly_animated', - 'plotly_sunburstclick', -]; +import {eventNames, getPlotlyEventName, getPropName, updateEvents} from './events.js'; // Check if a window is available since SSR (server-side rendering) // breaks unnecessarily if you try to use it server-side. @@ -59,10 +12,6 @@ const isBrowser = typeof window !== 'undefined'; const DEFAULT_DATA = Object.freeze([]); const DEFAULT_STYLE = Object.freeze({position: 'relative', display: 'inline-block'}); -function getPlotlyEventName(eventName) { - return 'plotly_' + eventName.toLowerCase(); -} - export default function plotComponentFactory(Plotly) { return forwardRef(function PlotlyComponent( { @@ -248,7 +197,7 @@ export default function plotComponentFactory(Plotly) { function syncEventHandlers() { eventNames.forEach((eventName) => { - const prop = eventProps['on' + eventName]; + const prop = eventProps[getPropName(eventName)]; const handler = handlersRef.current[eventName]; const hasHandler = Boolean(handler); if (prop && !hasHandler) { From e6f9c0cf4c4b0b9c0f26e0cc5e7ccd73f10be68b Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 11:39:47 -0600 Subject: [PATCH 2/8] Add missing types --- src/factory.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/factory.d.ts b/src/factory.d.ts index 34e75ea..cac00fe 100644 --- a/src/factory.d.ts +++ b/src/factory.d.ts @@ -49,6 +49,7 @@ export interface PlotParams { onFramework?: EventCallback; onHover?: EventCallback; onHoverAnywhere?: EventCallback; + onIcicleClick?: EventCallback; onLegendClick?: EventCallback; onLegendDoubleClick?: EventCallback; onRelayout?: EventCallback; @@ -63,6 +64,7 @@ export interface PlotParams { onSunburstClick?: EventCallback; onTransitioning?: EventCallback; onTransitionInterrupted?: EventCallback; + onTreemapClick?: EventCallback; onUnhover?: EventCallback; onWebGlContextLost?: EventCallback; } From dc508ad22b3b5b78be4549f1141777ac0b8e3ab9 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 11:49:56 -0600 Subject: [PATCH 3/8] Add events tests, fix plotly mock --- src/__mocks__/plotly.js | 17 ++++++++--- src/__tests__/events.test.js | 45 ++++++++++++++++++++++++++++++ src/__tests__/react-plotly.test.js | 30 ++++++++++++++++++++ 3 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/events.test.js diff --git a/src/__mocks__/plotly.js b/src/__mocks__/plotly.js index fe1ad31..5a77948 100644 --- a/src/__mocks__/plotly.js +++ b/src/__mocks__/plotly.js @@ -3,6 +3,17 @@ const state = {}; const ASYNC_DELAY = 1; +// Real plotly.js binds node's EventEmitter onto the graph div, so a gd exposes +// `removeListener` (see plotly.js src/lib/events.js). The `event-emitter` +// package only gives us `off`, and the wrapper treats a missing +// `removeListener` as "not a plotly graph div" — which silently disabled the +// whole update-event path under test. Alias it so the mock matches reality. +function attachEmitter(gd) { + EventEmitter(gd); // eslint-disable-line new-cap + gd.removeListener = gd.off; + return gd; +} + export default { plot: jest.fn((gd) => { state.gd = gd; @@ -11,16 +22,14 @@ export default { }, ASYNC_DELAY); }), newPlot: jest.fn((gd) => { - state.gd = gd; - EventEmitter(state.gd); // eslint-disable-line new-cap + state.gd = attachEmitter(gd); setTimeout(() => { state.gd.emit('plotly_afterplot'); }, ASYNC_DELAY); }), react: jest.fn((gd) => { - state.gd = gd; - EventEmitter(state.gd); // eslint-disable-line new-cap + state.gd = attachEmitter(gd); setTimeout(() => { state.gd.emit('plotly_afterplot'); diff --git a/src/__tests__/events.test.js b/src/__tests__/events.test.js new file mode 100644 index 0000000..794ce5a --- /dev/null +++ b/src/__tests__/events.test.js @@ -0,0 +1,45 @@ +import {readFileSync} from 'node:fs'; +import {join} from 'node:path'; +import {events, eventNames, getPlotlyEventName, getPropName, updateEvents} from '../events'; + +// `events` drives the runtime wiring. The two hand-maintained copies — the +// `on*` props in `factory.d.ts` and the README table — can't be derived from +// it, so they're checked here instead. The declarations are shipped to +// consumers but unused by this library, which is why a text comparison is +// enough; nothing here needs the type system. +describe('events', () => { + test('has no duplicate event names', () => { + expect(new Set(eventNames).size).toBe(eventNames.length); + }); + + test('every update event is one of the forwarded events', () => { + const forwarded = eventNames.map(getPlotlyEventName); + expect(updateEvents.length).toBeGreaterThan(0); + expect(forwarded).toEqual(expect.arrayContaining(updateEvents)); + }); + + test('PlotParams declares a prop for every event', () => { + const dts = readFileSync(join(__dirname, '..', 'factory.d.ts'), 'utf8'); + + // Lines look like: onAfterPlot?: EventCallback; + const propPattern = /^\s*(on\w+)\?: EventCallback;$/gm; + const declared = [...dts.matchAll(propPattern)].map(([, prop]) => prop); + + expect(declared).toEqual(events.map((event) => getPropName(event.name))); + }); + + test('README table matches the event list', () => { + const readme = readFileSync(join(__dirname, '..', '..', 'README.md'), 'utf8'); + + // Rows look like: | `onAfterPlot` | `Function` | `plotly_afterplot` | + const rowPattern = /^\|\s*`(on\w+)`\s*\|\s*`Function`\s*\|\s*`(plotly_\w+)`\s*\|$/gm; + const documented = [...readme.matchAll(rowPattern)].map(([, prop, event]) => [prop, event]); + + const expected = events.map((event) => [ + getPropName(event.name), + getPlotlyEventName(event.name), + ]); + + expect(documented).toEqual(expected); + }); +}); diff --git a/src/__tests__/react-plotly.test.js b/src/__tests__/react-plotly.test.js index bace500..5ee764c 100644 --- a/src/__tests__/react-plotly.test.js +++ b/src/__tests__/react-plotly.test.js @@ -2,6 +2,7 @@ import React, {StrictMode, useState} from 'react'; import {act, render} from '@testing-library/react'; import createComponent from '../factory'; +import {eventNames, getPlotlyEventName, getPropName} from '../events'; import once from 'onetime'; describe('', () => { @@ -203,6 +204,35 @@ describe('', () => { }) .catch((err) => done(err)); }); + + // Every forwarded event should reach its `on*` prop. Driving this from + // the event list means a new entry is covered without touching the test. + test.each(eventNames)('forwards plotly_%s to its prop', (eventName) => { + let received; + const handler = (evt) => { + received = evt; + }; + + return createPlot({[getPropName(eventName)]: handler}).then((plot) => { + plot.gd.emit(getPlotlyEventName(eventName), {eventName}); + expect(received).toEqual({eventName}); + }); + }); + + // The drill-down traces all share plotly.js' sunburst click handler, + // which mutates `trace.level` — so onUpdate has to fire for each of them + // or consumers never see the new level. See issue #375. + test.each(['SunburstClick', 'TreemapClick', 'IcicleClick'])( + 'fires onUpdate for %s drill-downs', + (eventName) => { + const onUpdate = jest.fn(); + + return createPlot({onUpdate}).then((plot) => { + plot.gd.emit(getPlotlyEventName(eventName)); + expect(onUpdate).toHaveBeenCalled(); + }); + } + ); }); describe('StrictMode', () => { From 1edac4af57a8441feb201cb5717e00fdf1fe466b Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 16:51:46 -0600 Subject: [PATCH 4/8] Remove cancelable events from updateEvents to avoid two listeners issue --- src/__tests__/react-plotly.test.js | 28 +++++++++++++++++++--------- src/events.js | 17 +++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/__tests__/react-plotly.test.js b/src/__tests__/react-plotly.test.js index 5ee764c..58e1ba2 100644 --- a/src/__tests__/react-plotly.test.js +++ b/src/__tests__/react-plotly.test.js @@ -219,20 +219,30 @@ describe('', () => { }); }); - // The drill-down traces all share plotly.js' sunburst click handler, - // which mutates `trace.level` — so onUpdate has to fire for each of them - // or consumers never see the new level. See issue #375. + // Cancelable — the consumer's handler must be the only listener or + // plotly could drop its `return false`. See the note in `events.js`. test.each(['SunburstClick', 'TreemapClick', 'IcicleClick'])( - 'fires onUpdate for %s drill-downs', + 'attaches only the consumer handler to %s', (eventName) => { - const onUpdate = jest.fn(); + const handler = () => false; - return createPlot({onUpdate}).then((plot) => { - plot.gd.emit(getPlotlyEventName(eventName)); - expect(onUpdate).toHaveBeenCalled(); - }); + return createPlot({[getPropName(eventName)]: handler, onUpdate: () => {}}).then( + (plot) => { + const listeners = [].concat(plot.gd.__ee__[getPlotlyEventName(eventName)] || []); + expect(listeners).toEqual([handler]); + } + ); } ); + + test('fires onUpdate when a drill-down animation completes', () => { + const onUpdate = jest.fn(); + + return createPlot({onUpdate}).then((plot) => { + plot.gd.emit('plotly_animated'); + expect(onUpdate).toHaveBeenCalled(); + }); + }); }); describe('StrictMode', () => { diff --git a/src/events.js b/src/events.js index 56774de..0946326 100644 --- a/src/events.js +++ b/src/events.js @@ -4,11 +4,12 @@ // - events are attached as `'plotly_' + name.toLowerCase()` // - react props are `'on' + name` // -// `triggersUpdate` marks events that plotly.js emits *after* mutating the -// figure itself (drill-downs, zooms, restyles). For those the wrapper has to -// re-read the graph div and fire `onUpdate`. Keeping the flag on the same -// record as the name is deliberate: a new drill-down event cannot be added -// without deciding whether it updates the figure. +// `triggersUpdate` marks events plotly.js emits *after* changing the figure; +// the wrapper listens to those and fires `onUpdate`. Never set it on a +// cancelable event — that adds a second listener alongside the consumer's, +// and plotly keeps only the last listener's return value, so a consumer's +// `return false` could be discarded. The drill-down clicks rely on +// `plotly_animated` instead. // // The `on*` prop types in `factory.d.ts` are maintained by hand against this list export const events = [ @@ -29,7 +30,7 @@ export const events = [ {name: 'Framework'}, {name: 'Hover'}, {name: 'HoverAnywhere'}, - {name: 'IcicleClick', triggersUpdate: true}, + {name: 'IcicleClick'}, {name: 'LegendClick'}, {name: 'LegendDoubleClick'}, {name: 'Relayout', triggersUpdate: true}, @@ -41,10 +42,10 @@ export const events = [ {name: 'SliderChange'}, {name: 'SliderEnd'}, {name: 'SliderStart'}, - {name: 'SunburstClick', triggersUpdate: true}, + {name: 'SunburstClick'}, {name: 'Transitioning'}, {name: 'TransitionInterrupted'}, - {name: 'TreemapClick', triggersUpdate: true}, + {name: 'TreemapClick'}, {name: 'Unhover'}, {name: 'WebGlContextLost'}, ]; From f17059b9f2f4b7d77032d08d333afdc111523327 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 17:06:11 -0600 Subject: [PATCH 5/8] Add missing plotly.js events --- src/events.js | 7 +++++-- src/factory.d.ts | 11 ++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/events.js b/src/events.js index 0946326..8f9410e 100644 --- a/src/events.js +++ b/src/events.js @@ -16,23 +16,25 @@ export const events = [ {name: 'AfterExport'}, {name: 'AfterPlot'}, {name: 'Animated', triggersUpdate: true}, + {name: 'Animating'}, {name: 'AnimatingFrame'}, {name: 'AnimationInterrupted'}, {name: 'AutoSize'}, {name: 'BeforeExport'}, {name: 'BeforeHover'}, + {name: 'BeforePlot'}, {name: 'ButtonClicked'}, {name: 'Click'}, {name: 'ClickAnnotation'}, - {name: 'ClickAnywhere'}, {name: 'Deselect'}, {name: 'DoubleClick', triggersUpdate: true}, {name: 'Framework'}, {name: 'Hover'}, - {name: 'HoverAnywhere'}, {name: 'IcicleClick'}, {name: 'LegendClick'}, {name: 'LegendDoubleClick'}, + {name: 'LegendTitleClick'}, + {name: 'LegendTitleDoubleClick'}, {name: 'Relayout', triggersUpdate: true}, {name: 'Relayouting', triggersUpdate: true}, {name: 'Restyle', triggersUpdate: true}, @@ -43,6 +45,7 @@ export const events = [ {name: 'SliderEnd'}, {name: 'SliderStart'}, {name: 'SunburstClick'}, + {name: 'Transitioned'}, {name: 'Transitioning'}, {name: 'TransitionInterrupted'}, {name: 'TreemapClick'}, diff --git a/src/factory.d.ts b/src/factory.d.ts index cac00fe..91631a1 100644 --- a/src/factory.d.ts +++ b/src/factory.d.ts @@ -35,23 +35,25 @@ export interface PlotParams { onAfterExport?: EventCallback; onAfterPlot?: EventCallback; onAnimated?: EventCallback; + onAnimating?: EventCallback; onAnimatingFrame?: EventCallback; onAnimationInterrupted?: EventCallback; onAutoSize?: EventCallback; onBeforeExport?: EventCallback; onBeforeHover?: EventCallback; + onBeforePlot?: EventCallback; onButtonClicked?: EventCallback; onClick?: EventCallback; onClickAnnotation?: EventCallback; - onClickAnywhere?: EventCallback; onDeselect?: EventCallback; onDoubleClick?: EventCallback; onFramework?: EventCallback; onHover?: EventCallback; - onHoverAnywhere?: EventCallback; onIcicleClick?: EventCallback; onLegendClick?: EventCallback; onLegendDoubleClick?: EventCallback; + onLegendTitleClick?: EventCallback; + onLegendTitleDoubleClick?: EventCallback; onRelayout?: EventCallback; onRelayouting?: EventCallback; onRestyle?: EventCallback; @@ -62,6 +64,7 @@ export interface PlotParams { onSliderEnd?: EventCallback; onSliderStart?: EventCallback; onSunburstClick?: EventCallback; + onTransitioned?: EventCallback; onTransitioning?: EventCallback; onTransitionInterrupted?: EventCallback; onTreemapClick?: EventCallback; @@ -80,8 +83,6 @@ export interface PlotParams { */ declare function createPlotlyComponent( Plotly: unknown -): React.ForwardRefExoticComponent< - PlotParams & React.RefAttributes ->; +): React.ForwardRefExoticComponent>; export default createPlotlyComponent; From 10b05f85b8072baed5ec255b687b404fcaf498b6 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 17:06:18 -0600 Subject: [PATCH 6/8] Fix typo --- src/__tests__/react-plotly.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/react-plotly.test.js b/src/__tests__/react-plotly.test.js index 58e1ba2..db23fae 100644 --- a/src/__tests__/react-plotly.test.js +++ b/src/__tests__/react-plotly.test.js @@ -186,7 +186,7 @@ describe('', () => { }); }); - describe('manging event handlers', () => { + describe('managing event handlers', () => { test('should add an event handler when one does not already exist', (done) => { let received; const onRelayout = (evt) => { From 793c875733142df1aac3a009a1cef4fef46082a5 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 17:06:39 -0600 Subject: [PATCH 7/8] Update README events table --- README.md | 89 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 16ee701..2a5a170 100644 --- a/README.md +++ b/README.md @@ -152,41 +152,60 @@ The `onInitialized`, `onUpdate` and `onPurge` props are all functions which will Event handlers for specific [`plotly.js` events](https://plotly.com/javascript/plotlyjs-events/) may be attached through the following props: -| Prop | Type | Plotly Event | -| ------------------------- | ---------- | ------------------------------ | -| `onAfterExport` | `Function` | `plotly_afterexport` | -| `onAfterPlot` | `Function` | `plotly_afterplot` | -| `onAnimated` | `Function` | `plotly_animated` | -| `onAnimatingFrame` | `Function` | `plotly_animatingframe` | -| `onAnimationInterrupted` | `Function` | `plotly_animationinterrupted` | -| `onAutoSize` | `Function` | `plotly_autosize` | -| `onBeforeExport` | `Function` | `plotly_beforeexport` | -| `onBeforeHover` | `Function` | `plotly_beforehover` | -| `onButtonClicked` | `Function` | `plotly_buttonclicked` | -| `onClick` | `Function` | `plotly_click` | -| `onClickAnnotation` | `Function` | `plotly_clickannotation` | -| `onClickAnywhere` | `Function` | `plotly_clickanywhere` | -| `onDeselect` | `Function` | `plotly_deselect` | -| `onDoubleClick` | `Function` | `plotly_doubleclick` | -| `onFramework` | `Function` | `plotly_framework` | -| `onHover` | `Function` | `plotly_hover` | -| `onHoverAnywhere` | `Function` | `plotly_hoveranywhere` | -| `onLegendClick` | `Function` | `plotly_legendclick` | -| `onLegendDoubleClick` | `Function` | `plotly_legenddoubleclick` | -| `onRelayout` | `Function` | `plotly_relayout` | -| `onRelayouting` | `Function` | `plotly_relayouting` | -| `onRestyle` | `Function` | `plotly_restyle` | -| `onRedraw` | `Function` | `plotly_redraw` | -| `onSelected` | `Function` | `plotly_selected` | -| `onSelecting` | `Function` | `plotly_selecting` | -| `onSliderChange` | `Function` | `plotly_sliderchange` | -| `onSliderEnd` | `Function` | `plotly_sliderend` | -| `onSliderStart` | `Function` | `plotly_sliderstart` | -| `onSunburstClick` | `Function` | `plotly_sunburstclick` | -| `onTransitioning` | `Function` | `plotly_transitioning` | -| `onTransitionInterrupted` | `Function` | `plotly_transitioninterrupted` | -| `onUnhover` | `Function` | `plotly_unhover` | -| `onWebGlContextLost` | `Function` | `plotly_webglcontextlost` | +| Prop | Type | Plotly Event | +| -------------------------- | ---------- | ------------------------------- | +| `onAfterExport` | `Function` | `plotly_afterexport` | +| `onAfterPlot` | `Function` | `plotly_afterplot` | +| `onAnimated` | `Function` | `plotly_animated` | +| `onAnimating` | `Function` | `plotly_animating` | +| `onAnimatingFrame` | `Function` | `plotly_animatingframe` | +| `onAnimationInterrupted` | `Function` | `plotly_animationinterrupted` | +| `onAutoSize` | `Function` | `plotly_autosize` | +| `onBeforeExport` | `Function` | `plotly_beforeexport` | +| `onBeforeHover` | `Function` | `plotly_beforehover` | +| `onBeforePlot` | `Function` | `plotly_beforeplot` | +| `onButtonClicked` | `Function` | `plotly_buttonclicked` | +| `onClick` | `Function` | `plotly_click` | +| `onClickAnnotation` | `Function` | `plotly_clickannotation` | +| `onDeselect` | `Function` | `plotly_deselect` | +| `onDoubleClick` | `Function` | `plotly_doubleclick` | +| `onFramework` | `Function` | `plotly_framework` | +| `onHover` | `Function` | `plotly_hover` | +| `onIcicleClick` | `Function` | `plotly_icicleclick` | +| `onLegendClick` | `Function` | `plotly_legendclick` | +| `onLegendDoubleClick` | `Function` | `plotly_legenddoubleclick` | +| `onLegendTitleClick` | `Function` | `plotly_legendtitleclick` | +| `onLegendTitleDoubleClick` | `Function` | `plotly_legendtitledoubleclick` | +| `onRelayout` | `Function` | `plotly_relayout` | +| `onRelayouting` | `Function` | `plotly_relayouting` | +| `onRestyle` | `Function` | `plotly_restyle` | +| `onRedraw` | `Function` | `plotly_redraw` | +| `onSelected` | `Function` | `plotly_selected` | +| `onSelecting` | `Function` | `plotly_selecting` | +| `onSliderChange` | `Function` | `plotly_sliderchange` | +| `onSliderEnd` | `Function` | `plotly_sliderend` | +| `onSliderStart` | `Function` | `plotly_sliderstart` | +| `onSunburstClick` | `Function` | `plotly_sunburstclick` | +| `onTransitioned` | `Function` | `plotly_transitioned` | +| `onTransitioning` | `Function` | `plotly_transitioning` | +| `onTransitionInterrupted` | `Function` | `plotly_transitioninterrupted` | +| `onTreemapClick` | `Function` | `plotly_treemapclick` | +| `onUnhover` | `Function` | `plotly_unhover` | +| `onWebGlContextLost` | `Function` | `plotly_webglcontextlost` | + +To receive `onClick` or `onHover` for positions that are not over a trace, set +`clickanywhere` or `hoveranywhere` in your `layout`. These are layout attributes +rather than distinct events: the ordinary `plotly_click` / `plotly_hover` fire +with an empty `points` array, plus `xvals` / `yvals` — arrays of the cursor +position in data space, one entry per axis. + +```javascript + console.log(e.points, e.xvals, e.yvals)} +/> +``` ## Examples From 8b353e8498e6910ea90c01db4d3e06463fa26dc3 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 17:29:27 -0600 Subject: [PATCH 8/8] Update CHANGELOG --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a68fe..f8f7053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,22 @@ where X.Y.Z is the semver of most recent react-plotly.js release. ## [Unreleased] +### Added + +- Added missing `onTreemapClick` and `onIcicleClick` event props for the `plotly_treemapclick` and `plotly_icicleclick` events [[#376](https://github.com/plotly/react-plotly.js/pull/376)] +- Added additional missing events: `onLegendTitleClick`, `onLegendTitleDoubleClick`, `onBeforePlot`, `onAnimating`, and `onTransitioned` [[#376](https://github.com/plotly/react-plotly.js/pull/376)] + +### Fixed + +- Returning `false` from `onSunburstClick`, `onTreemapClick`, or `onIcicleClick` now prevents the drill-down [[#376](https://github.com/plotly/react-plotly.js/pull/376)] + - `onUpdate` now fires for these via `plotly_animated`, so it reports the figure after the new `level` is applied and no longer fires when the drill-down is cancelled + +### Removed + +- Removed the errantly added `onClickAnywhere` and `onHoverAnywhere` props [[#376](https://github.com/plotly/react-plotly.js/pull/376)] + - plotly.js has no `plotly_clickanywhere` or `plotly_hoveranywhere` events, so these never fired + - `clickanywhere` and `hoveranywhere` are **layout attributes** that widen the ordinary `plotly_click` / `plotly_hover`, so the feature is reached through `onClick` / `onHover` with the layout flag set — see the README + ## [4.0.0] - 2026-06-18 ### Added @@ -38,7 +54,8 @@ where X.Y.Z is the semver of most recent react-plotly.js release. ### Added -- `onClickAnywhere` and `onHoverAnywhere` event props for the corresponding `plotly_clickanywhere` and `plotly_hoveranywhere` events introduced in plotly.js v3 [[#360](https://github.com/plotly/react-plotly.js/pull/360)] +- `onClickAnywhere` and `onHoverAnywhere` event props [[#360](https://github.com/plotly/react-plotly.js/pull/360)] + - **Correction:** plotly.js v3 introduced `clickanywhere` / `hoveranywhere` as _layout attributes_, not as events, so these props never fired. They are removed in the next release. ### Changed