Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,16 @@ jobs:
- name: Type-check TypeScript sources
run: npm run typecheck

test-types:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could we just add test-types as a second step in the typecheck job above? Is there a reason you want a second runner?

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
Expand Down
3 changes: 3 additions & 0 deletions draftlogs/8126_fix.md
Original file line number Diff line number Diff line change
@@ -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`
2 changes: 2 additions & 0 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export type * from '../src/types/generated/schema';

export type {
ButtonClickEvent,
DataUpdate,
Icon,
LayoutUpdate,
ModeBarButton,
ModeBarButtonAny,
ModeBarDefaultButtons,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
35 changes: 27 additions & 8 deletions src/types/core/api.d.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overloads you add here are good, but can still report "errors" on perfectly valid code.

Example:

import * as Plotly from 'plotly.js';
type RelayoutArg = Parameters<typeof Plotly.relayout>[1];
const wrapped: RelayoutArg = { autosize: true };

There is a red squiggle as typescript evaluates against the "last" overload and completely misses the other valid overload.
Typescript says to put the most generic signature last. What do you think about including a generic "catch-all" overload at the end for these kinds of cases?

export function relayout(root: Root, astr: string | LayoutUpdate, val?: any): Promise<PlotlyHTMLElement>;

The same comment applies to the restyle overloads.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -208,19 +208,38 @@ export function newPlot(
figure: PlotlyDataLayoutConfig
): Promise<PlotlyHTMLElement>;

/** Update layout properties on an existing plot. */
export function relayout(root: Root, layout: Partial<Layout>): Promise<PlotlyHTMLElement>;
/**
* 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<PlotlyHTMLElement>;
/**
* 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<PlotlyHTMLElement>;
/** Re-render the plot at `root` from its current data/layout. */
export function redraw(root: Root): Promise<PlotlyHTMLElement>;
/** 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<PlotlyHTMLElement>;
/**
* 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<PlotlyHTMLElement>;
export function restyle(root: Root, astr: string, val: any, traces?: number[] | number): Promise<PlotlyHTMLElement>;

/**
* Update both trace and layout properties in a single call.
Expand All @@ -231,8 +250,8 @@ export function restyle(root: Root, aobj: Data, traces?: number[] | number): Pro
*/
export function update(
root: Root,
traceUpdate: Data,
layoutUpdate: Partial<Layout>,
traceUpdate: DataUpdate,
layoutUpdate: LayoutUpdate,
traces?: number[] | number
): Promise<PlotlyHTMLElement>;

Expand Down
8 changes: 5 additions & 3 deletions src/types/core/events.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -149,10 +150,11 @@ export interface PlotRestyleEventUpdate {
export type PlotRestyleEvent = [PlotRestyleEventUpdate, number[]];

/**
* Payload for `plotly_relayout`. Carries a `Partial<Layout>` 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<Layout> {
export interface PlotRelayoutEvent extends LayoutUpdate {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type contains 6 hand-crafted keys (xaxis.range[0], etc.) but LayoutUpdate doesn't type these keys at all.
This starts to look like 2 sources of truth for these types. And already, the two definitions are drifting:
the hand-crafted keys here define values that are narrower than in LayoutUpdate. xaxis.range[0]?: number misses date axes with values like 2026-10-16.

What do you think about moving the hand-typed keys here into LayoutUpdate with the type widened to include date strings? That way there would be a single definition.

/** 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. */
Expand Down
27 changes: 27 additions & 0 deletions src/types/core/layout.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,30 @@ export interface Template {
/** Template layout defaults. */
layout?: Partial<Layout> | 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<Layout> & 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<Data> & AttributePathKeys;
26 changes: 26 additions & 0 deletions test/types/relayout_event.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions test/types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["./**/*.ts"],
"exclude": []
}
78 changes: 78 additions & 0 deletions test/types/update_api.ts
Original file line number Diff line number Diff line change
@@ -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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
* Nothing here runs. `npm run typecheck` compiles this file against the public entry point.
* Nothing here runs. `npm run test-types` 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 } };
Loading