From 16be55c8c4fc07cde6fe5ddad6fd34a98a916d30 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 15 Oct 2026 17:08:52 -0600 Subject: [PATCH 1/3] fix(types): Accept attribute paths in plotly_relayout, relayout, restyle, and update --- lib/index.d.ts | 2 ++ src/types/core/api.d.ts | 35 +++++++++++++++++++++++++++-------- src/types/core/events.d.ts | 8 +++++--- src/types/core/layout.d.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 583bd7811a3..da892f0a9f8 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -55,7 +55,9 @@ export type * from '../src/types/generated/schema'; export type { ButtonClickEvent, + DataUpdate, Icon, + LayoutUpdate, ModeBarButton, ModeBarButtonAny, ModeBarDefaultButtons, diff --git a/src/types/core/api.d.ts b/src/types/core/api.d.ts index b0694bff5ba..622846f294a 100644 --- a/src/types/core/api.d.ts +++ b/src/types/core/api.d.ts @@ -5,7 +5,7 @@ import type { AnimationOpts, Data, Frame, Layout } from '../generated/schema'; import type { Config, DownloadImgopts, ToImgopts } from './config'; import type { PlotlyHTMLElement } from './events'; -import type { Icon, Template } from './layout'; +import type { DataUpdate, Icon, LayoutUpdate, Template } from './layout'; // --------------------------------------------------------------------------- // Roots and request shapes @@ -208,19 +208,38 @@ export function newPlot( figure: PlotlyDataLayoutConfig ): Promise; -/** Update layout properties on an existing plot. */ -export function relayout(root: Root, layout: Partial): Promise; +/** + * Update layout properties on an existing plot with an update object. + * + * @param layout - Update object whose keys are layout properties or attribute paths + */ +export function relayout(root: Root, layout: LayoutUpdate): Promise; +/** + * Update one layout property on an existing plot with an attribute path. + * + * @param astr - Attribute path, for example `'xaxis.autorange'` + * @param val - New value of the attribute + */ +export function relayout(root: Root, astr: string, val: any): Promise; /** Re-render the plot at `root` from its current data/layout. */ export function redraw(root: Root): Promise; /** Remove a plot and its event listeners from the DOM. */ export function purge(root: Root): void; /** - * Update trace properties (style) on the existing plot. + * Update trace properties (style) on the existing plot with an update object. + * + * @param aobj - Update object whose keys are trace properties or attribute paths + * @param traces - Trace index/indices to update (defaults to all) + */ +export function restyle(root: Root, aobj: DataUpdate, traces?: number[] | number): Promise; +/** + * Update one trace property on the existing plot with an attribute path. * - * @param aobj - Update object whose keys are attribute paths + * @param astr - Attribute path, for example `'marker.color'` + * @param val - New value of the attribute, or an array with one value for each trace * @param traces - Trace index/indices to update (defaults to all) */ -export function restyle(root: Root, aobj: Data, traces?: number[] | number): Promise; +export function restyle(root: Root, astr: string, val: any, traces?: number[] | number): Promise; /** * Update both trace and layout properties in a single call. @@ -231,8 +250,8 @@ export function restyle(root: Root, aobj: Data, traces?: number[] | number): Pro */ export function update( root: Root, - traceUpdate: Data, - layoutUpdate: Partial, + traceUpdate: DataUpdate, + layoutUpdate: LayoutUpdate, traces?: number[] | number ): Promise; diff --git a/src/types/core/events.d.ts b/src/types/core/events.d.ts index 616fd80bebd..a6690a6a102 100644 --- a/src/types/core/events.d.ts +++ b/src/types/core/events.d.ts @@ -17,6 +17,7 @@ import type { } from '../generated/schema'; import type { Datum } from '../lib/common'; import type { Config } from './config'; +import type { LayoutUpdate } from './layout'; // --------------------------------------------------------------------------- // Point / datum types in events @@ -149,10 +150,11 @@ export interface PlotRestyleEventUpdate { export type PlotRestyleEvent = [PlotRestyleEventUpdate, number[]]; /** - * Payload for `plotly_relayout`. Carries a `Partial` plus a few - * common bracketed axis-range/autorange keys that flatten nested updates. + * Payload for `plotly_relayout`. Carries a `LayoutUpdate`, so any attribute + * path is a valid key, plus typed values for a few common axis-range and + * autorange keys that flatten nested updates. */ -export interface PlotRelayoutEvent extends Partial { +export interface PlotRelayoutEvent extends LayoutUpdate { /** New x-axis range minimum, when the user changed the range. */ 'xaxis.range[0]'?: number; /** New x-axis range maximum, when the user changed the range. */ diff --git a/src/types/core/layout.d.ts b/src/types/core/layout.d.ts index 4ede682efae..2d147d24fec 100644 --- a/src/types/core/layout.d.ts +++ b/src/types/core/layout.d.ts @@ -157,3 +157,30 @@ export interface Template { /** Template layout defaults. */ layout?: Partial | undefined; } + +// --------------------------------------------------------------------------- +// Update objects (relayout / restyle / update) +// --------------------------------------------------------------------------- + +/** Keys that hold an attribute path such as `'xaxis.range[0]'` or `'annotations[0].text'`. */ +interface AttributePathKeys { + [key: `${string}.${string}`]: any; + [key: `${string}[${string}`]: any; +} + +/** + * Layout update for `Plotly.relayout` and `Plotly.update`. + * + * A key is a layout property or an attribute path, for example `'xaxis.range[0]'` or `'annotations[0].text'`. + * An attribute path changes one property and keeps its siblings. A nested object replaces the whole container. + * A value behind an attribute path has the type `any`. + */ +export type LayoutUpdate = Partial & AttributePathKeys; + +/** + * Trace update for `Plotly.restyle` and `Plotly.update`. + * + * A key is a trace property or an attribute path, for example `'marker.color'`. + * A value behind an attribute path has the type `any`. + */ +export type DataUpdate = Partial & AttributePathKeys; From ecb5486fa257035e2a5a7b4f003ccea9d6eaecba Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 15 Oct 2026 17:18:59 -0600 Subject: [PATCH 2/3] Add type tests locally, in CI --- .github/workflows/ci.yml | 10 +++++ package.json | 1 + test/types/relayout_event.ts | 26 ++++++++++++ test/types/tsconfig.json | 5 +++ test/types/update_api.ts | 78 ++++++++++++++++++++++++++++++++++++ 5 files changed, 120 insertions(+) create mode 100644 test/types/relayout_event.ts create mode 100644 test/types/tsconfig.json create mode 100644 test/types/update_api.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c084e08400d..74068705a97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -586,6 +586,16 @@ jobs: - name: Type-check TypeScript sources run: npm run typecheck + test-types: + needs: install-and-cibuild + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - uses: ./.github/actions/setup-workspace + + - name: Test the public TypeScript types + run: npm run test-types + generated-types-drift: needs: install-and-cibuild runs-on: ubuntu-latest diff --git a/package.json b/package.json index 005e41bf051..8137db5734a 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "test-mock": "node tasks/test_mock.mjs", "test-image": "node test/image/compare_pixels_test.mjs", "test-export": "node test/image/export_test.js", + "test-types": "tsc --noEmit -p test/types", "test-syntax": "tsx tasks/test_syntax.js && npm run find-strings -- --no-output", "test-bundle": "node tasks/test_bundle.js", "test-plain-obj": "node tasks/test_plain_obj.mjs", diff --git a/test/types/relayout_event.ts b/test/types/relayout_event.ts new file mode 100644 index 00000000000..3d6d7963100 --- /dev/null +++ b/test/types/relayout_event.ts @@ -0,0 +1,26 @@ +/** + * Consumer-side checks for the `plotly_relayout` event payload. + * + * Nothing here runs. See `update_api.ts`. + */ + +import type { PlotRelayoutEvent } from '../../lib/index'; + +declare const event: PlotRelayoutEvent; + +// Typed keys keep their value types +const rangeStart: number | undefined = event['xaxis.range[0]']; +const autorange: boolean | undefined = event['xaxis.autorange']; + +// @ts-expect-error the typed range key holds a number, not a string +const rangeStartAsString: string | undefined = event['xaxis.range[0]']; + +// Any other attribute path is readable +const secondAxisStart = event['xaxis2.range[0]']; +const shapeStart = event['shapes[0].x0']; + +// Layout properties are still typed +const autosize: boolean | undefined = event.autosize; + +// @ts-expect-error unknown top-level key +const typo = event.titl; diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 00000000000..dea162a5ca2 --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./**/*.ts"], + "exclude": [] +} diff --git a/test/types/update_api.ts b/test/types/update_api.ts new file mode 100644 index 00000000000..1e6aa97b886 --- /dev/null +++ b/test/types/update_api.ts @@ -0,0 +1,78 @@ +/** + * Consumer-side checks for the `relayout`, `restyle` and `update` types. + * + * Nothing here runs. `npm run typecheck` compiles this file against the public entry point. + * A line marked `@ts-expect-error` must fail to compile, so the build breaks if a type becomes too permissive. + */ + +import * as Plotly from '../../lib/index'; + +declare const gd: HTMLElement; + +// --------------------------------------------------------------------------- +// relayout +// --------------------------------------------------------------------------- + +Plotly.relayout(gd, { 'xaxis.autorange': true }); +Plotly.relayout(gd, { 'xaxis.range[0]': 0, 'xaxis.range[1]': 5 }); +Plotly.relayout(gd, { 'xaxis.title.font.size': 14 }); +Plotly.relayout(gd, { 'annotations[0].text': 'hello' }); +Plotly.relayout(gd, { 'updatemenus[0].buttons[1].label': 'hello' }); +Plotly.relayout(gd, { xaxis: { autorange: true } }); +Plotly.relayout(gd, { autosize: true }); +Plotly.relayout(gd, 'xaxis.autorange', true); +Plotly.relayout(gd, 'annotations[0].text', 'hello'); + +// @ts-expect-error top-level typo +Plotly.relayout(gd, { titl: 'abc' }); +// @ts-expect-error wrong value type on a known top-level key +Plotly.relayout(gd, { autosize: 'yes' }); +// @ts-expect-error the string form needs a value +Plotly.relayout(gd, 'xaxis.autorange'); + +// --------------------------------------------------------------------------- +// restyle +// --------------------------------------------------------------------------- + +Plotly.restyle(gd, { 'marker.color': 'red' }); +Plotly.restyle(gd, { 'marker.color': 'red' }, [0]); +Plotly.restyle(gd, { 'line.width': 2, 'marker.size[0]': 10 }, 1); +Plotly.restyle(gd, { visible: false }); +Plotly.restyle(gd, 'marker.color', 'red'); +Plotly.restyle(gd, 'marker.color', ['red', 'blue'], [0, 1]); + +// @ts-expect-error top-level typo +Plotly.restyle(gd, { colr: 'red' }); +// @ts-expect-error wrong value type on a known top-level key +Plotly.restyle(gd, { visible: 'maybe' }); +// @ts-expect-error the string form needs a value +Plotly.restyle(gd, 'marker.color'); + +// --------------------------------------------------------------------------- +// update +// --------------------------------------------------------------------------- + +Plotly.update(gd, { 'marker.color': 'red' }, { 'xaxis.range[0]': 0 }); +Plotly.update(gd, { 'marker.color': 'red' }, { 'xaxis.range[0]': 0 }, [0]); +Plotly.update(gd, { visible: false }, { autosize: true }); + +// @ts-expect-error top-level typo in the trace update +Plotly.update(gd, { colr: 'red' }, { autosize: true }); +// @ts-expect-error top-level typo in the layout update +Plotly.update(gd, { visible: false }, { titl: 'abc' }); + +// --------------------------------------------------------------------------- +// APIs that take a full figure must stay strict about unknown keys +// --------------------------------------------------------------------------- + +Plotly.newPlot(gd, [], { autosize: true }); +Plotly.validate([], { autosize: true }); + +// @ts-expect-error a dotted path is not a valid key in a full layout +Plotly.newPlot(gd, [], { 'xaxis.autorange': true }); +// @ts-expect-error a dotted typo must not pass in a full layout +Plotly.newPlot(gd, [], { 'xaxis.autrange': true }); +// @ts-expect-error a dotted path is not a valid key when validating a layout +Plotly.validate([], { 'xaxis.autorange': true }); +// @ts-expect-error a dotted path is not a valid key in a template layout +const template: Plotly.Template = { layout: { 'xaxis.autorange': true } }; From 166aa66f2934f5c8872368007667d0054cf248c7 Mon Sep 17 00:00:00 2001 From: Cameron DeCoster Date: Thu, 15 Oct 2026 17:39:30 -0600 Subject: [PATCH 3/3] Add draftlog --- draftlogs/8126_fix.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 draftlogs/8126_fix.md diff --git a/draftlogs/8126_fix.md b/draftlogs/8126_fix.md new file mode 100644 index 00000000000..af15d6ed89f --- /dev/null +++ b/draftlogs/8126_fix.md @@ -0,0 +1,3 @@ +- Update TypeScript types so that `Plotly.relayout`, `Plotly.restyle`, `Plotly.update`, and the `plotly_relayout` event accept attribute paths such as `'xaxis.autorange'` [[#8126](https://github.com/plotly/plotly.js/pull/8126)] + - Add the `LayoutUpdate` and `DataUpdate` types for these update objects + - Add the `(root, attribute, value)` call form to the types of `Plotly.relayout` and `Plotly.restyle`