From 2d8ae17fda7e385462eb2c5cbe8fd767a2436f8f Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 10 Jun 2026 15:51:18 -0600 Subject: [PATCH 01/16] Update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a82ce94..e152aff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,23 @@ where X.Y.Z is the semver of most recent react-plotly.js release. ## [Unreleased] +## [4.0.0] - 2026-06-11 + +### Added + +- Added native ESM output alongside CJS [[#367](https://github.com/plotly/react-plotly.js/pull/367)] + - Published artifacts now live under `dist/` instead of at the repo root; the `main` entry moved from `react-plotly.js` to `./dist/index.cjs` + - Dropped the unminified UMD bundle + - `import Plot from 'react-plotly.js'` now resolves correctly in Rolldown and any other strict ESM environment +- Added TypeScript declaration files (`index.d.ts` / `factory.d.ts`) [[#368](https://github.com/plotly/react-plotly.js/pull/368)] + +### Changed + +- Update linting dependencies [[#366](https://github.com/plotly/react-plotly.js/pull/366)] +- Refactored the wrapper from a class component to a functional component using hooks [[#369](https://github.com/plotly/react-plotly.js/pull/369)] + - Refs forwarded to `` now resolve to the rendered `
` element (the plotly graph div) directly via `forwardRef`, instead of to the class instance + - **Migration:** replace `ref.current.el` with `ref.current` + ## [3.0.0] - 2026-06-09 ### Added From 3bfd1c8bb10f4d3e0cb11f6eeb83abde12d99415 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 10 Jun 2026 15:51:42 -0600 Subject: [PATCH 02/16] Update README per v4 changes --- README.md | 227 ++++++++++++++++++++++++++++-------------- plotly-react-logo.png | Bin 0 -> 3171 bytes 2 files changed, 155 insertions(+), 72 deletions(-) create mode 100644 plotly-react-logo.png diff --git a/README.md b/README.md index 5552775..0ede7f5 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,9 @@ # react-plotly.js -![plotly-react-logo](https://images.plot.ly/plotly-documentation/thumbnail/react.png) +![plotly-react-logo](plotly-react-logo.png) > A [plotly.js](https://github.com/plotly/plotly.js) React component from -> [Plotly](https://plot.ly/). The basis of Plotly's -> [React component suite](https://plot.ly/products/react/). - -👉 [DEMO](http://react-plotly.js-demo.getforge.io/) - -👉 [Demo source code](https://github.com/plotly/react-plotly.js-demo-app) +> [Plotly](https://plotly.com/).
@@ -24,9 +19,14 @@ - [Quick start](#quick-start) - [State management](#state-management) - [Refreshing the Plot](#refreshing-the-plot) -- [API](#api) +- [API](#api-reference) - [Basic props](#basic-props) - [Event handler props](#event-handler-props) +- [Examples](#examples) + - [Responsive plot](#responsive-plot) + - [Event handlers](#event-handlers) + - [TypeScript](#typescript) + - [Grabbing the graph div via `ref`](#grabbing-the-graph-div-via-ref) - [Customizing the `plotly.js` bundle](#customizing-the-plotlyjs-bundle) - [Loading from a ` + + + ``` -And instantiate the component with +React is pinned to 18 here because React 19 stopped shipping UMD builds; consumers wanting React 19 should load it via [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) or use a bundler. + +The factory is exposed as the global `createPlotlyComponent`. Build the component and mount it: ```javascript const Plot = createPlotlyComponent(Plotly); - -ReactDOM.render( +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( React.createElement(Plot, { data: [{x: [1, 2, 3], y: [2, 1, 3]}], - }), - document.getElementById('root') + }) ); ``` -You can see an example of this method in action -[here](https://codepen.io/rsreusser/pen/qPgwwJ?editors=1010). - ## Development To get started: @@ -240,18 +315,26 @@ To get started: $ npm install ``` -To transpile from ES2015 + JSX into the ES5 npm-distributed version: +To build the published artifacts (`dist/index.{mjs,cjs}`, `dist/factory.{mjs,cjs}`, the UMD bundle, and the declaration files) via [tsup](https://tsup.egoist.dev/): ```bash -$ npm run prepublishOnly +$ npm run build ``` -To run the tests: +To run lint + typecheck + jest: ```bash $ npm run test ``` +To watch source files and rebuild on change: + +```bash +$ npm run watch +``` + +Releases are cut from `master` per the steps in [`RELEASE.md`](./RELEASE.md). + ## License -© 2017-2020 Plotly, Inc. MIT License. +© 2017-2026 Plotly, Inc. MIT License. diff --git a/plotly-react-logo.png b/plotly-react-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..29a5c5cd16d5ecd58ce19ad1bbcd76695d70e7a9 GIT binary patch literal 3171 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 GZ~h1R Date: Wed, 10 Jun 2026 15:59:51 -0600 Subject: [PATCH 03/16] Add RELEASE document --- RELEASE.md | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 RELEASE.md diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..7ec718a --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,105 @@ +# Releasing react-plotly.js + +This document covers how to cut a new release of `react-plotly.js` to npm. +It's evergreen — substitute `X.Y.Z` with the target version (e.g. `3.0.0`) +and `vX.Y.Z` with the git tag (e.g. `v3.0.0`). + +## Prerequisites + +- npm publish access to the [`react-plotly.js`](https://www.npmjs.com/package/react-plotly.js) package. Granted by an existing maintainer via the npm `plotly` org. +- npm two-factor authentication configured (`npm profile get`). Publishes require an OTP. +- Push access to `plotly/react-plotly.js` on GitHub, including tag-creation permission. +- A clean local clone of the repo, ideally with `node_modules` reinstalled fresh against `master`. + +## Release sequence + +### 1. Pre-flight checks + +From the branch that will become the release and in a clean state (typically `master`, or an integration branch like `vN` that's about to merge into `master`): + +```bash +npm install # fresh install against the current lockfile +npm test # lint + typecheck + jest +npm run build # verifies the tsup build pipeline (ESM + CJS + UMD bundles) +npm run clean # removes build artifacts (also runs as part of prepublishOnly) +``` + +`npm test` and `npm run build` must succeed. CI on the source branch should also be green. + +### 2. Update the changelog + +`CHANGELOG.md` follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). For each release: + +1. Rename the `## [Unreleased]` heading to `## [X.Y.Z] - YYYY-MM-DD` using the actual release date. +2. Add a fresh empty `## [Unreleased]` heading above it. + +Section order within a release: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security` (omit any that have no entries). Use past-tense verbs. Link each entry to the PR that introduced it, and credit external contributors with `, with thanks to @user for the contribution!` at the end of the line. + +### 3. Bump the version + +```bash +npm version X.Y.Z --no-git-tag-version +``` + +`--no-git-tag-version` prevents npm from tagging immediately; the tag is created later, after the release PR has merged. + +### 4. Open the release PR + +Branch the release off the appropriate parent — the integration branch (e.g. `vN`) for a major cumulative release, or `master` for a minor/patch. Branch name convention: `release-X.Y.Z`. + +```bash +git checkout vN # or master, depending on the release type +git pull +git checkout -b release-X.Y.Z +git add * +git commit -m "chore: release X.Y.Z" +git push -u origin release-X.Y.Z +gh pr create --base master --title "chore: release X.Y.Z" \ + --body "See CHANGELOG.md entry for [$X.Y.Z]." +``` + +When branching from an integration branch, the PR carries all of that branch's commits plus the release-prep commits — reviewers see the full changeset alongside the version bump. + +### 5. Merge and tag + +Once the PR is reviewed, CI is green, and merged into `master`: + +```bash +git checkout master +git pull +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +The tag must point at the merge commit on `master`. + +### 6. Publish to npm + +```bash +npm publish +``` + +This triggers `prepublishOnly`, which runs `npm run build` (tsup, which cleans `dist/` and emits the ESM + CJS + UMD bundles + declaration files) against the current working tree before publishing. npm will prompt for the OTP from your authenticator. + +After publish, confirm: + +```bash +npm view react-plotly.js version # should report X.Y.Z +npm view react-plotly.js dist-tags # `latest` should be X.Y.Z +``` + +### 7. Create the GitHub release + +Via the GitHub UI: *Releases → Draft a new release*, target tag `vX.Y.Z`, paste the relevant `CHANGELOG.md` section into the body. + +## Pre-release versions + +For release candidates or beta builds, append a pre-release identifier and publish under a non-`latest` dist-tag so existing consumers aren't auto-upgraded: + +```bash +npm version X.Y.Z-rc.0 --no-git-tag-version +# ...PR + merge + tag as above, with tag vX.Y.Z-rc.0... +npm publish --tag next +``` + +Consumers can opt in with `npm install react-plotly.js@next`. From 06917bb9b62307bf6121a2ca51863b04368f62b2 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 10 Jun 2026 16:01:07 -0600 Subject: [PATCH 04/16] chore: Version updates for v4.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1c5defe..6f37d29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-plotly.js", - "version": "3.0.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-plotly.js", - "version": "3.0.0", + "version": "4.0.0", "license": "MIT", "devDependencies": { "@babel/core": "^7.19.0", diff --git a/package.json b/package.json index ce0382d..d9bb854 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-plotly.js", - "version": "3.0.0", + "version": "4.0.0", "license": "MIT", "description": "A plotly.js react component from Plotly", "author": "Plotly, Inc.", From 26d8d0c999df11eb25317c4fc548ec71c3221003 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 18 Jun 2026 12:30:54 -0600 Subject: [PATCH 05/16] Update CHANGELOG --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e152aff..ec23f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ where X.Y.Z is the semver of most recent react-plotly.js release. ## [Unreleased] -## [4.0.0] - 2026-06-11 +## [4.0.0] - 2026-06-18 ### Added @@ -28,6 +28,12 @@ where X.Y.Z is the semver of most recent react-plotly.js release. - Refs forwarded to `` now resolve to the rendered `
` element (the plotly graph div) directly via `forwardRef`, instead of to the class instance - **Migration:** replace `ref.current.el` with `ref.current` +### Fixed + +- `onPurge` now fires and `Plotly.purge` runs on unmount [[#372](https://github.com/plotly/react-plotly.js/pull/372)] +- Plot is re-initialized correctly after React StrictMode's dev-only simulated unmount/remount [[#372](https://github.com/plotly/react-plotly.js/pull/372)] +- TypeScript declarations now expose ref forwarding so consumers can attach a typed `ref` prop without a TS error [[#373](https://github.com/plotly/react-plotly.js/pull/373)] + ## [3.0.0] - 2026-06-09 ### Added From 2bde8365ac02415c8469964452edb2e6f91ea31a Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 18 Jun 2026 13:15:19 -0600 Subject: [PATCH 06/16] chore: Update docs references from master to main --- CHANGELOG.md | 2 +- README.md | 2 +- RELEASE.md | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec23f94..06a68fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ For more context information, please read through the To see all merged commits on the default branch that will be part of the next plotly.js release, go to: - + where X.Y.Z is the semver of most recent react-plotly.js release. diff --git a/README.md b/README.md index 0ede7f5..16ee701 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,7 @@ To watch source files and rebuild on change: $ npm run watch ``` -Releases are cut from `master` per the steps in [`RELEASE.md`](./RELEASE.md). +Releases are cut from `main` per the steps in [`RELEASE.md`](./RELEASE.md). ## License diff --git a/RELEASE.md b/RELEASE.md index 7ec718a..28d4737 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -9,13 +9,13 @@ and `vX.Y.Z` with the git tag (e.g. `v3.0.0`). - npm publish access to the [`react-plotly.js`](https://www.npmjs.com/package/react-plotly.js) package. Granted by an existing maintainer via the npm `plotly` org. - npm two-factor authentication configured (`npm profile get`). Publishes require an OTP. - Push access to `plotly/react-plotly.js` on GitHub, including tag-creation permission. -- A clean local clone of the repo, ideally with `node_modules` reinstalled fresh against `master`. +- A clean local clone of the repo, ideally with `node_modules` reinstalled fresh against `main`. ## Release sequence ### 1. Pre-flight checks -From the branch that will become the release and in a clean state (typically `master`, or an integration branch like `vN` that's about to merge into `master`): +From the branch that will become the release and in a clean state (typically `main`, or an integration branch like `vN` that's about to merge into `main`): ```bash npm install # fresh install against the current lockfile @@ -45,16 +45,16 @@ npm version X.Y.Z --no-git-tag-version ### 4. Open the release PR -Branch the release off the appropriate parent — the integration branch (e.g. `vN`) for a major cumulative release, or `master` for a minor/patch. Branch name convention: `release-X.Y.Z`. +Branch the release off the appropriate parent — the integration branch (e.g. `vN`) for a major cumulative release, or `main` for a minor/patch. Branch name convention: `release-X.Y.Z`. ```bash -git checkout vN # or master, depending on the release type +git checkout vN # or main, depending on the release type git pull git checkout -b release-X.Y.Z git add * git commit -m "chore: release X.Y.Z" git push -u origin release-X.Y.Z -gh pr create --base master --title "chore: release X.Y.Z" \ +gh pr create --base main --title "chore: release X.Y.Z" \ --body "See CHANGELOG.md entry for [$X.Y.Z]." ``` @@ -62,16 +62,16 @@ When branching from an integration branch, the PR carries all of that branch's c ### 5. Merge and tag -Once the PR is reviewed, CI is green, and merged into `master`: +Once the PR is reviewed, CI is green, and merged into `main`: ```bash -git checkout master +git checkout main git pull git tag vX.Y.Z git push origin vX.Y.Z ``` -The tag must point at the merge commit on `master`. +The tag must point at the merge commit on `main`. ### 6. Publish to npm From 6e66ccdbadf40d8ec870496d20b1c50d8b42e601 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Tue, 28 Jul 2026 11:39:08 -0600 Subject: [PATCH 07/16] 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 08/16] 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 09/16] 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 10/16] 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 11/16] 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 12/16] 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 13/16] 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 14/16] 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 From f58e64d2751c9acac9f0ba74d7cc3db9c6862b73 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Wed, 29 Jul 2026 16:13:27 -0600 Subject: [PATCH 15/16] chore: Update files for v4.1.0 --- CHANGELOG.md | 2 ++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f7053..0832539 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ where X.Y.Z is the semver of most recent react-plotly.js release. ## [Unreleased] +## [4.1.0] - 2026-07-29 + ### 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)] diff --git a/package-lock.json b/package-lock.json index 6f37d29..e97dcb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-plotly.js", - "version": "4.0.0", + "version": "4.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "react-plotly.js", - "version": "4.0.0", + "version": "4.1.0", "license": "MIT", "devDependencies": { "@babel/core": "^7.19.0", diff --git a/package.json b/package.json index d9bb854..0be2c0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-plotly.js", - "version": "4.0.0", + "version": "4.1.0", "license": "MIT", "description": "A plotly.js react component from Plotly", "author": "Plotly, Inc.", From 1967d127a51ac672698cc000a80b1fc11c831acb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:18:07 +0000 Subject: [PATCH 16/16] Bump form-data from 4.0.5 to 4.0.6 Bumps [form-data](https://github.com/form-data/form-data) from 4.0.5 to 4.0.6. - [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md) - [Commits](https://github.com/form-data/form-data/compare/v4.0.5...v4.0.6) --- updated-dependencies: - dependency-name: form-data dependency-version: 4.0.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e97dcb2..3828e83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6980,17 +6980,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6"