From d48f59f73771ac0c28f77973bad8e50aea205cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Farzat?= Date: Tue, 13 Oct 2026 15:52:07 -0300 Subject: [PATCH 1/4] fix: improve plot event typing and responsiveness --- MIGRATION.md | 30 +++- README.md | 38 ++++- .../plotly/src/lib/plotly.component.spec.ts | 50 ++++++- projects/plotly/src/lib/plotly.component.ts | 132 ++++++++++++------ projects/plotly/src/lib/plotly.interface.ts | 35 +++++ 5 files changed, 236 insertions(+), 49 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index c5ca0ef..0ab342f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -55,18 +55,42 @@ package compatible. | --- | --- | --- | | `2.x` | 9 | Not compatible with Angular 13 | | `3.x` | 10 | Angular 10 library-format release | -| `5.x` | 13 | Use this line for Angular 13 | -| `6.x` | 16 | Angular 16 release | +| `4.x` | 11–12 | Declares Angular greater than 10.0.9 | +| `5.x` | 13–15 | Angular 13 compatibility line | +| `6.x` | 16–19 | Declares Angular 16 or newer; validate your application before a major Angular upgrade | +| `20.x` | 20 | Angular 20 compatibility line | +| `21.x` | 21 | Peer dependencies are capped below Angular 22 | +| `22.x` | 22 | Current compatibility line; peer dependencies are capped below Angular 23 | The package version does not increase for every Angular major. Check the package peer dependencies before upgrading across an Angular major, and use the package line that declares support for the Angular version in the application. +### Plotly.js bundles and Angular 19 + +`angular-plotly.js` does not install a Plotly.js bundle for an application. +The bundle is selected by the application and passed to `PlotlyModule.forRoot` +or `PlotlyService.setPlotly`. The library's own tested baseline is currently +`plotly.js-dist` 2.35.3. + +If an Angular 19 test build fails to resolve the `d3-*` modules from +`plotly.js-dist-min` 3.x, pin the bundle to the known-good 2.x line while +investigating the application build configuration: + +```bash +npm install plotly.js-dist-min@^2.35.3 +``` + +The wrapper only requires a Plotly object with `newPlot`/`react`; the missing +module errors occur while Angular bundles Plotly.js, before the wrapper runs. +Use a small production build and a test build to validate a move to Plotly.js +3.x before changing the pin. + ### Angular 13 follow-up notes Version 5.0.0 migrated the library to the Ivy engine and removed Protractor and Codelyzer from the library development setup. Review application code and CI configuration for those Angular ecosystem changes as part of the Angular 13 migration. The Plotly component API is otherwise unchanged in the release -notes for 5.0.0. \ No newline at end of file +notes for 5.0.0. diff --git a/README.md b/README.md index abff0e7..e8f5c8e 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,34 @@ You should see a plot like this: Example plot

+### Standalone applications + +`PlotlyComponent` is standalone. Configure the Plotly bundle once, then import +the component wherever a chart is rendered: + +```typescript +import { Component } from '@angular/core'; +import * as PlotlyJS from 'plotly.js-dist-min'; +import { PlotlyComponent, PlotlyService } from 'angular-plotly.js'; + +PlotlyService.setPlotly(PlotlyJS); + +@Component({ + standalone: true, + imports: [PlotlyComponent], + template: '', +}) +export class SalesChartComponent { + data = [{ x: [1, 2, 3], y: [2, 6, 3], type: 'scatter' }]; + layout = { autosize: true }; +} +``` + +To keep Plotly out of the initial bundle, put this setup in a component loaded +through a lazy route. The `plotly.js-dist-min` import then belongs to that lazy +chunk. Do not configure `PlotlyModule.forRoot()` in application-wide providers +when the chart itself is intended to be lazy-loaded. + For a full description of Plotly chart types and attributes see the following resources: @@ -161,19 +189,19 @@ Event handlers for specific [`plotly.js` events](https://plot.ly/javascript/plot | `(autoSize)` | `Function` | `plotly_autosize` | | | `(beforeExport)` | `Function` | `plotly_beforeexport` | | | `(buttonClicked)` | `Function` | `plotly_buttonclicked` | | -| `(plotlyClick)` | `Function` | `plotly_click` | [why not (click)?](FAQ.md#why-using-plotlyclick-instead-of-click) | +| `(plotlyClick)` | `Plotly.PlotMouseEvent` | `plotly_click` | [why not (click)?](FAQ.md#why-using-plotlyclick-instead-of-click) | | `(clickAnnotation)` | `Function` | `plotly_clickannotation` | | | `(deselect)` | `Function` | `plotly_deselect` | | | `(doubleClick)` | `Function` | `plotly_doubleclick` | | | `(framework)` | `Function` | `plotly_framework` | | -| `(hover)` | `Function` | `plotly_hover` | | +| `(hover)` | `Plotly.PlotMouseEvent` | `plotly_hover` | | | `(legendClick)` | `Function` | `plotly_legendclick` | | | `(legendDoubleClick)` | `Function` | `plotly_legenddoubleclick` | | | `(react)` | `Function` | `plotly_react` | | -| `(relayout)` | `Function` | `plotly_relayout` | | -| `(restyle)` | `Function` | `plotly_restyle` | | +| `(relayout)` | `Plotly.PlotRelayoutEvent` | `plotly_relayout` | | +| `(restyle)` | `Plotly.PlotRestyleEvent` | `plotly_restyle` | | | `(redraw)` | `Function` | `plotly_redraw` | | -| `(selected)` | `Function` | `plotly_selected` | | +| `(selected)` | `Plotly.PlotSelectionEvent` | `plotly_selected` | | | `(selecting)` | `Function` | `plotly_selecting` | | | `(sliderChange)` | `Function` | `plotly_sliderchange` | | | `(sliderEnd)` | `Function` | `plotly_sliderend` | | diff --git a/projects/plotly/src/lib/plotly.component.spec.ts b/projects/plotly/src/lib/plotly.component.spec.ts index 52568ef..e74daa5 100644 --- a/projects/plotly/src/lib/plotly.component.spec.ts +++ b/projects/plotly/src/lib/plotly.component.spec.ts @@ -1,7 +1,8 @@ -import { SimpleChange, ComponentRef } from '@angular/core'; +import { Component, SimpleChange, ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PlotlyComponent } from './plotly.component'; +import { Plotly } from './plotly.interface'; import { PlotlyService } from './plotly.service'; import PlotlyJS from 'plotly.js-dist'; @@ -9,6 +10,17 @@ import PlotlyJS from 'plotly.js-dist'; PlotlyService.setPlotly(PlotlyJS); +@Component({ + standalone: true, + imports: [PlotlyComponent], + template: '', +}) +class TypedEventHostComponent { + capture(event: Plotly.PlotMouseEvent): void { + void event.points; + } +} + describe('PlotlyComponent', () => { let component: PlotlyComponent; let fixture: ComponentFixture; @@ -19,7 +31,7 @@ describe('PlotlyComponent', () => { windowSpy = jasmine.createSpyObj('Window', ['addEventListener', 'removeEventListener']); TestBed.configureTestingModule({ - imports: [PlotlyComponent], + imports: [PlotlyComponent, TypedEventHostComponent], providers: [ PlotlyService, ], @@ -198,6 +210,40 @@ describe('PlotlyComponent', () => { expect(deprecatedClick).toHaveBeenCalledWith(event); }); + it('should type-check plotlyClick handlers in standalone templates', () => { + expect(TestBed.createComponent(TypedEventHostComponent).componentInstance).toBeTruthy(); + }); + + it('should resize an autosized plot after a layout update', async () => { + const instance = document.createElement('div') as any; + component.plotlyInstance = instance; + componentRef.setInput('layout', { autosize: true }); + spyOn(component.plotly, 'update').and.resolveTo(instance); + spyOn(component.plotly, 'resize'); + spyOn(component, 'getWindow').and.returnValue({ + requestAnimationFrame: (callback: FrameRequestCallback) => { + callback(0); + return 1; + }, + } as Window); + + await component.updatePlot(); + + expect(component.plotly.resize).toHaveBeenCalledWith(instance); + }); + + it('should not resize after a layout update with explicit dimensions', async () => { + const instance = document.createElement('div') as any; + component.plotlyInstance = instance; + componentRef.setInput('layout', { autosize: true, height: 400 }); + spyOn(component.plotly, 'update').and.resolveTo(instance); + spyOn(component.plotly, 'resize'); + + await component.updatePlot(); + + expect(component.plotly.resize).not.toHaveBeenCalled(); + }); + it('should skip Plotly initialization on the server', () => { const serverComponent = TestBed.runInInjectionContext(() => new PlotlyComponent( component.plotly, diff --git a/projects/plotly/src/lib/plotly.component.ts b/projects/plotly/src/lib/plotly.component.ts index 8f8a178..dd82249 100644 --- a/projects/plotly/src/lib/plotly.component.ts +++ b/projects/plotly/src/lib/plotly.component.ts @@ -41,9 +41,11 @@ export class PlotlyComponent implements OnInit, OnChanges, OnDestroy, DoCheck { public plotlyInstance?: Plotly.PlotlyHTMLElement; public resizeHandler?: (instance: Plotly.PlotlyHTMLElement) => void; + public resizeObserver?: ResizeObserver; public layoutDiffer?: KeyValueDiffer; public dataDiffer?: IterableDiffer; private destroyed = false; + private resizeAnimationFrame?: number; @ViewChild('plot', { static: true }) plotEl!: ElementRef; @@ -72,49 +74,49 @@ export class PlotlyComponent implements OnInit, OnChanges, OnDestroy, DoCheck { purge = output(); error = output(); - afterExport = output(); - afterPlot = output(); - animated = output(); - animatingFrame = output(); - animationInterrupted = output(); - autoSize = output(); - beforeExport = output(); - beforeHover = output(); - buttonClicked = output(); + afterExport = output(); + afterPlot = output(); + animated = output(); + animatingFrame = output(); + animationInterrupted = output(); + autoSize = output(); + beforeExport = output(); + beforeHover = output(); + buttonClicked = output(); /** * @deprecated DEPRECATED: Reconsider using `(plotlyClick)` instead of `(click)` to avoid event conflict. Please check https://github.com/plotly/angular-plotly.js#FAQ */ - click = output(); - plotlyClick = output(); - clickAnnotation = output(); - deselect = output(); - doubleClick = output(); - framework = output(); - hover = output(); - legendClick = output(); - legendDoubleClick = output(); + click = output(); + plotlyClick = output(); + clickAnnotation = output(); + deselect = output(); + doubleClick = output(); + framework = output(); + hover = output(); + legendClick = output(); + legendDoubleClick = output(); /** * @deprecated DEPRECATED: Event react is not list as an plotly.js event */ - react = output(); - relayout = output(); - relayouting = output(); - restyle = output(); - redraw = output(); - selected = output(); - selecting = output(); - sliderChange = output(); - sliderEnd = output(); - sliderStart = output(); - sunburstclick = output(); - transitioning = output(); - transitionInterrupted = output(); - unhover = output(); + react = output(); + relayout = output(); + relayouting = output(); + restyle = output(); + redraw = output(); + selected = output(); + selecting = output(); + sliderChange = output(); + sliderEnd = output(); + sliderStart = output(); + sunburstclick = output(); + transitioning = output(); + transitionInterrupted = output(); + unhover = output(); /** * @deprecated DEPRECATED: Event treemapclick is not list as an plotly.js event */ - treemapclick = output(); - webglcontextlost = output(); + treemapclick = output(); + webglcontextlost = output(); public eventNames = ['afterExport', 'afterPlot', 'animated', 'animatingFrame', 'animationInterrupted', 'autoSize', @@ -149,6 +151,12 @@ export class PlotlyComponent implements OnInit, OnChanges, OnDestroy, DoCheck { this.getWindow().removeEventListener('resize', this.resizeHandler as any); this.resizeHandler = undefined; } + this.resizeObserver?.disconnect(); + this.resizeObserver = undefined; + if (this.resizeAnimationFrame !== undefined) { + this.getWindow().cancelAnimationFrame?.(this.resizeAnimationFrame); + this.resizeAnimationFrame = undefined; + } if (this.plotlyInstance) { const figure = this.createFigure(); @@ -254,14 +262,14 @@ export class PlotlyComponent implements OnInit, OnChanges, OnDestroy, DoCheck { this.eventNames.forEach(name => { const eventName = `plotly_${name.toLowerCase()}`; - const event = (this as any)[name] as OutputEmitterRef; + const event = (this as any)[name] as OutputEmitterRef; - plotlyInstance.on(eventName, (data: any) => event.emit(data)); + plotlyInstance.on(eventName, (data: unknown) => event.emit(data as Plotly.Event)); }); - plotlyInstance.on('plotly_click', (data: any) => { - this.plotlyClick.emit(data); - this.click.emit(data); + plotlyInstance.on('plotly_click', (data: unknown) => { + this.plotlyClick.emit(data as Plotly.PlotMouseEvent); + this.click.emit(data as Plotly.PlotMouseEvent); }); this.updateWindowResizeHandler(); @@ -299,6 +307,7 @@ export class PlotlyComponent implements OnInit, OnChanges, OnDestroy, DoCheck { this.config(), this.frames() ).then(() => { + this.scheduleResizeAfterLayoutUpdate(); const figure = this.createFigure(); this.update.emit(figure); }, err => { @@ -319,6 +328,51 @@ export class PlotlyComponent implements OnInit, OnChanges, OnDestroy, DoCheck { this.resizeHandler = undefined; } } + + this.updateResizeObserver(); + } + + private updateResizeObserver(): void { + const shouldObserve = this.useResizeHandler() + && this.plotlyInstance + && isPlatformBrowser(this.platformId) + && typeof ResizeObserver !== 'undefined'; + + if (shouldObserve) { + if (this.resizeObserver === undefined) { + this.resizeObserver = new ResizeObserver(() => this.scheduleResize()); + this.resizeObserver.observe(this.plotEl.nativeElement); + } + } else { + this.resizeObserver?.disconnect(); + this.resizeObserver = undefined; + } + } + + private scheduleResizeAfterLayoutUpdate(): void { + const layout = this.layout(); + if (layout?.['autosize'] === true && layout['height'] === undefined && layout['width'] === undefined) { + this.scheduleResize(); + } + } + + private scheduleResize(): void { + if (this.destroyed || !this.plotlyInstance || !isPlatformBrowser(this.platformId) || this.resizeAnimationFrame !== undefined) { + return; + } + + const requestAnimationFrame = this.getWindow().requestAnimationFrame; + if (typeof requestAnimationFrame !== 'function') { + this.plotly.resize(this.plotlyInstance); + return; + } + + this.resizeAnimationFrame = requestAnimationFrame.call(this.getWindow(), () => { + this.resizeAnimationFrame = undefined; + if (!this.destroyed && this.plotlyInstance) { + this.plotly.resize(this.plotlyInstance); + } + }); } dataDifferTrackBy(_: number, item: any): unknown { diff --git a/projects/plotly/src/lib/plotly.interface.ts b/projects/plotly/src/lib/plotly.interface.ts index 9991610..664914f 100644 --- a/projects/plotly/src/lib/plotly.interface.ts +++ b/projects/plotly/src/lib/plotly.interface.ts @@ -4,6 +4,41 @@ export namespace Plotly { export type Layout = any; export type Config = any; + /** Base payload emitted by plotly.js events. */ + export interface Event { + [key: string]: unknown; + } + + /** A point included in click, hover, and selection event payloads. */ + export interface EventPoint extends Event { + curveNumber?: number; + pointNumber?: number | number[]; + pointIndex?: number | number[]; + x?: unknown; + y?: unknown; + z?: unknown; + text?: unknown; + customdata?: unknown; + data?: Data; + fullData?: Data; + } + + export interface PlotMouseEvent extends Event { + points: EventPoint[]; + event?: MouseEvent; + } + + export interface PlotSelectionEvent extends PlotMouseEvent { + range?: Record; + lassoPoints?: { x: number[]; y: number[] }; + } + + export interface PlotRelayoutEvent extends Event { + event?: Event; + } + + export type PlotRestyleEvent = [Record, number[]]; + export interface Figure { data: Data[]; layout: Partial; From 51c5f73fd89ef39f4579e153fbc6c7215dec9ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Farzat?= Date: Tue, 13 Oct 2026 16:11:35 -0300 Subject: [PATCH 2/4] docs: add unreleased changelog entry --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5184803..9ff6be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [Unreleased] +### Added +- Added typed Plotly event payloads for click, hover, selection, relayout, and + restyle outputs, including standalone-template type-check coverage. +- Added standalone Angular and lazy-loading guidance. +- Expanded the Angular compatibility reference and documented the Angular 19 + Plotly.js bundle workaround. + +### Fixed +- Resize autosized plots after layout updates and observe responsive plot + container size changes when `[useResizeHandler]` is enabled. + ## [22.0.1] - 2026-08-20 ### Fixed - Added `[innerStyle]` as the preferred plot container styling input while From df4a9232241f915e26f26466dde431bfe9ae7ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Farzat?= Date: Tue, 13 Oct 2026 16:12:38 -0300 Subject: [PATCH 3/4] chore: release 22.0.2 --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- projects/plotly/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff6be7..864c504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [22.0.2] - 2026-10-13 ### Added - Added typed Plotly event payloads for click, hover, selection, relayout, and restyle outputs, including standalone-template type-check coverage. diff --git a/package-lock.json b/package-lock.json index 025fa68..cad2ec6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "angular-plotly.js", - "version": "22.0.1", + "version": "22.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "angular-plotly.js", - "version": "22.0.1", + "version": "22.0.2", "license": "MIT", "dependencies": { "@angular/common": "22.2.0", diff --git a/package.json b/package.json index c01e140..7854c64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "angular-plotly.js", - "version": "22.0.1", + "version": "22.0.2", "license": "MIT", "scripts": { "ng": "ng", diff --git a/projects/plotly/package.json b/projects/plotly/package.json index 60c8a67..249db8e 100644 --- a/projects/plotly/package.json +++ b/projects/plotly/package.json @@ -1,6 +1,6 @@ { "name": "angular-plotly.js", - "version": "22.0.1", + "version": "22.0.2", "license": "MIT", "peerDependencies": { "@angular/common": ">=22.0.0 <23.0.0", From b6709dc6c1cc24a66a1f49ad2a15f490b8d2ad70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Farzat?= Date: Tue, 13 Oct 2026 16:23:42 -0300 Subject: [PATCH 4/4] docs: move compatibility table to readme --- MIGRATION.md | 19 ++----------------- README.md | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 0ab342f..8ed0845 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -49,23 +49,8 @@ package compatible. ng test --watch=false ``` -### Compatibility reference - -| `angular-plotly.js` | Angular | Notes | -| --- | --- | --- | -| `2.x` | 9 | Not compatible with Angular 13 | -| `3.x` | 10 | Angular 10 library-format release | -| `4.x` | 11–12 | Declares Angular greater than 10.0.9 | -| `5.x` | 13–15 | Angular 13 compatibility line | -| `6.x` | 16–19 | Declares Angular 16 or newer; validate your application before a major Angular upgrade | -| `20.x` | 20 | Angular 20 compatibility line | -| `21.x` | 21 | Peer dependencies are capped below Angular 22 | -| `22.x` | 22 | Current compatibility line; peer dependencies are capped below Angular 23 | - -The package version does not increase for every Angular major. Check the -package peer dependencies before upgrading across an Angular major, and use -the package line that declares support for the Angular version in the -application. +The [README compatibility table](README.md#angular-compatibility) is the +single reference for matching `angular-plotly.js` and Angular versions. ### Plotly.js bundles and Angular 19 diff --git a/README.md b/README.md index e8f5c8e..d728f13 100644 --- a/README.md +++ b/README.md @@ -14,26 +14,33 @@ --- +## Angular compatibility + Since version 20, the angular-plotly.js major follows the supported Angular major. Install the matching package line: -| angular-plotly.js | Angular | Branch | Support | +| `angular-plotly.js` | Angular | Branch | Support / notes | | --- | --- | --- | --- | -| 22.x | 22.x | `master` | Active | -| 21.x | 21.x | `v21` | LTS | -| 20.x | 20.x | `v20` | LTS through 2026-11-28 | +| `2.x` | 9 | — | Historical; not compatible with Angular 13 | +| `3.x` | 10 | — | Historical Angular 10 library-format release | +| `4.x` | 11–12 | — | Historical; declares Angular greater than 10.0.9 | +| `5.x` | 13–15 | — | Historical Angular 13 compatibility line | +| `6.x` | 16–19 | — | Historical; declares Angular 16 or newer; validate before a major Angular upgrade | +| `20.x` | 20 | `v20` | LTS through 2026-11-28 | +| `21.x` | 21 | `v21` | LTS; peer dependencies are capped below Angular 22 | +| `22.x` | 22 | `master` | Active; peer dependencies are capped below Angular 23 | Maintenance and release details are documented in [MAINTENANCE.md](MAINTENANCE.md). If you want to use with Angular 8.x, please use version [`angular-plotly.js@1.x`](https://github.com/plotly/angular-plotly.js/tree/1.x). -For upgrade instructions and historical Angular/package compatibility, see -the [migration guide](MIGRATION.md). +For upgrade instructions, see the [migration guide](MIGRATION.md). ## Content * [Installation](#installation) +* [Angular compatibility](#angular-compatibility) * [Migration guide](MIGRATION.md) * [Quick start](#quick-start) * [API](#api-reference)