diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5184803..864c504 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [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.
+- 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
diff --git a/MIGRATION.md b/MIGRATION.md
index c5ca0ef..8ed0845 100644
--- a/MIGRATION.md
+++ b/MIGRATION.md
@@ -49,19 +49,28 @@ package compatible.
ng test --watch=false
```
-### Compatibility reference
+The [README compatibility table](README.md#angular-compatibility) is the
+single reference for matching `angular-plotly.js` and Angular versions.
-| `angular-plotly.js` | Angular | Notes |
-| --- | --- | --- |
-| `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 |
+### Plotly.js bundles and Angular 19
-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.
+`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
@@ -69,4 +78,4 @@ 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..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)
@@ -97,6 +104,34 @@ You should see a plot like this:
+### 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 +196,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/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",
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;