UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

280 lines (239 loc) • 14.2 kB
# Type System Architecture How TypeScript types are organized in plotly.js. ## Three layers ``` ┌──────────────────────────────────────────────────────────────┐ │ Consumer surface (what `npm install plotly.js` exposes) │ │ lib/index.d.ts — wired via package.json#types │ │ `export type *` for generated schema types + explicit │ │ re-exports of hand-written types + `export as namespace │ │ Plotly` for global/namespace usage. │ └──────────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Internal authoring surface │ │ src/types/index.d.ts — re-exports everything (internal) │ └──────────────────────────────────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────────────┐ ┌────────────────────────────────┐ │ Hand-written types │ │ Generated types │ │ src/types/core/*.d.ts │ │ src/types/generated/... │ │ src/types/lib/*.d.ts │ │ │ │ │ │ schema.d.ts — common enums, │ │ │ │ traces, layout, animation, │ │ │ │ config, _internal namespace │ │ │ │ │ │ │ │ entry_points/ — one │ │ │ │ declaration per lib/ path │ └──────────────────────────┘ └────────────────────────────────┘ ``` `lib/index.d.ts` deliberately omits internal types (`FullLayout`, `FullData`, `GraphDiv`, the `AttributeMap` machinery, etc.) so consumers see a clean public API. Internal types live in `.internal.d.ts` files and remain accessible to plotly.js's own code through the `src/types/` re-exports. The split: - **Generated types** (`src/types/generated`) are the authoritative TypeScript representation of the runtime schema. The schema itself is produced from Plotly's JS attribute files (`src/.../attributes.js`), which remain the source of truth: chain is **attribute files → `plot-schema.json` → generated types**. - **`src/types/generated/schema.d.ts`** contains: - **Common enum aliases** discovered from the schema (Calendar, Dash, AxisType, PatternShape, XRef, YRef, TransitionEasing, TraceType — plus a deprecated `PlotType` alias for back-compat). - **Shared sub-interfaces** extracted from repeated subtrees (Font, FontArray, ColorBar, HoverLabel, Domain, Pattern, TickFormatStops, LegendGroupTitle). - **Per-trace data interfaces** for all trace types (BarData, ScatterData, IndicatorData, etc.), plus the **`Data` discriminated union** over all of them (`Partial<BarData> | Partial<ScatterData> | …`) — narrowed via the `type` field. - **Layout component interfaces** (LayoutAxis, Legend, Scene, Annotation, Shape, Slider, UpdateMenu, etc.) and the Layout interface itself. - **Animation, frames, and config interfaces** (Transition, AnimationFrameOpts, AnimationOpts, Frame, Edits) generated from `schema.animation`, `schema.frames`, and `schema.config.edits`. - **`_internal` namespace** wrapping types whose direct names would mislead consumers (Marker is scatter-only, Line is the marker outline) or are schema-internal helpers (AutoRangeOptions, Lighting, ErrorY). Reachable as `_internal.Marker` etc. but not at the top level. Generated from `plot-schema.json` by `tasks/generate_schema_types.mjs`. Run `npm run schema` to regenerate. - **Hand-written types** (everything in `src/types/` besides the `generated/` directory) cover everything the schema doesn't describe: events, internal runtime state, public API function signatures, utility types (Color, Datum, MarkerSymbol, ErrorBar), behavioral types (ModeBarButton, Icon, etc.). ## Public vs. private (the underscore convention) Plotly's runtime stores two kinds of state on graph elements: - **Public** — user-supplied, what `Plotly.newPlot(gd, data, layout)` accepts - **Private** (`_` prefix) — fully-resolved versions Plotly computes after applying defaults, defined modules, and so on This split is reflected in the types: | User-facing | Internal | Where defined | |---|---|---| | `Layout` | `FullLayout` | `Layout` in `generated/schema.d.ts`; `FullLayout` in `core/layout.internal.d.ts` | | `Data` (union over `type`) | `FullData` | `Data` in `generated/schema.d.ts` (union of schema `*Data` interfaces); `FullData = Data & FullDataInternals` in `core/data.internal.d.ts` | | (n/a) | `GraphDiv` (the `gd` param) | `core/graph-div.internal.d.ts` — DOM element with `_fullLayout`, `_fullData`, `calcdata`, etc. | `FullData` is the discriminated union of schema trace types intersected with the internal `_`-prefixed fields. Internal code that narrows on `trace.type` gets trace-specific fields plus the internal state in the same expression. Internal types use index signatures (`[key: string]: any`) liberally to allow incremental migration without blocking. As `_` properties get discovered during JS→TS conversion, add them to `FullLayout` or `FullDataInternals` (the file-local intersection target inside `data.internal.d.ts`). ## Directory layout ``` src/types/ ├── index.d.ts # main re-export hub (public + internal) ├── core/ # hand-written types for the core API │ ├── api.d.ts # public API function signatures (newPlot, etc.) │ ├── config.d.ts # Config, ToImgopts (Edits re-exported from generated) │ ├── data.internal.d.ts # CalcData, FullData │ ├── events.d.ts # PlotMouseEvent, PlotlyHTMLElement, etc. │ ├── graph-div.internal.d.ts # GraphDiv, GraphContext │ ├── layout.d.ts # ModeBar behavioral types, Template │ └── layout.internal.d.ts # FullLayout, LayoutSize, SubplotInfo │ ├── lib/ # primitives + the schema-extraction machinery │ ├── common.d.ts # Color, Datum, TypedArray, AxisName, ... │ └── attributes.d.ts # AttributeMap, AttrInfo (compile-time validation) │ └── generated/ # machine-generated types ├── schema.d.ts # all traces + layout + shared types (from plot-schema.json) └── entry_points/ # one declaration per lib/ entry point ├── core.d.ts # plotly.js/lib/core ├── scatter.d.ts # plotly.js/lib/scatter, and one per trace └── locales/ # plotly.js/lib/locales/<id>, one per locale ``` ### The `generated/entry_points/` directory `lib/` holds one entry point per trace, component, bundle, and locale, so a consumer can import `plotly.js/lib/scatter` instead of the whole library. Each entry point needs its own declaration. Those declarations live here rather than beside the entry points so `lib/` stays readable. `typesVersions` in package.json maps `plotly.js/lib/<entry>` onto this directory. Two things follow from that: - The `lib/index.d.ts` key in that table is necessary. TypeScript 5 runs the resolved `types` field back through the table, and without the key the `lib/*` pattern captures it and breaks the plain `plotly.js` import. - WARNING: TypeScript ignores `typesVersions` once a package has an `exports` field. Adding `exports` to package.json breaks every subpath declaration here. Run `npm run entry-point-types` to regenerate. Run `npm run entry-point-types-check` to fail when the committed output is stale. CI runs the check, so a new entry point cannot ship without its declaration. ### The `.internal.d.ts` convention Files with the `.internal.d.ts` suffix contain types that are **not** part of the public API (not re-exported in `lib/index.d.ts`). These are internal runtime types used only within plotly.js itself — `FullLayout`, `FullData`, `GraphDiv`, etc. If a file has no `.internal` suffix, all its exports are public. ## How schema type generation works ``` test/plot-schema.json (runtime schema: traces + layout + animation + config) │ ▼ [ tasks/generate_schema_types.mjs ] │ │ 0. Discover common enum aliases via COMMON_TYPE_ANCHORS │ (Calendar/Dash/AxisType/PatternShape/XRef/YRef/TransitionEasing, │ plus TraceType derived from the trace-names list, plus a deprecated │ `PlotType` alias) │ 1. Fingerprint every container subtree across traces and layout │ 2. Extract shared interfaces (Font, ColorBar, HoverLabel, etc.). │ Inject Transition and AnimationFrameOpts as shared types │ (animation has < MIN_OCCURRENCES sites otherwise) │ 3. Emit per-trace interfaces referencing shared types │ 4. Emit layout component interfaces (LayoutAxis, Legend, Scene, etc.) │ and the Layout interface with subplot index signatures │ 5. Emit AnimationOpts (from schema.animation), Frame (from │ schema.frames with field overrides for the recursive data/layout │ fields), and Edits (from schema.config.edits) │ 6. Wrap names in INTERNAL_INTERFACES inside `export namespace _internal` │ and rewrite outside-namespace references to `_internal.X` │ ▼ src/types/generated/schema.d.ts │ // Common enum aliases │ export type Calendar = 'chinese' | 'coptic' | ...; │ export type TraceType = 'bar' | 'scatter' | ...; │ /** @deprecated Renamed to TraceType. */ │ export type PlotType = TraceType; │ // Shared interfaces (public) │ export interface Font { ... } │ export interface ColorBar { ... } │ // Internal namespace │ export namespace _internal { │ export interface Marker { ... } │ export interface Line { ... } │ // ... │ } │ // Trace interfaces │ export interface ScatterData { marker?: _internal.Marker; ... } │ export interface BarData { ... } │ // Discriminated union over all traces │ export type Data = Partial<BarData> | Partial<ScatterData> | ...; │ // Layout │ export interface LayoutAxis { autorangeoptions?: _internal.AutoRangeOptions; ... } │ export interface Layout { ... } │ // Animation / config │ export interface AnimationOpts { transition?: Transition; ... } │ export interface Frame { data?: any[]; layout?: Partial<Layout>; ... } │ export interface Edits { ... } │ ▼ src/types/index.d.ts re-exports all types (internal authoring index). lib/index.d.ts uses `export type * from '.../generated/schema'` so every public schema-derived type is automatically re-exported to consumers. ``` Regenerate with `npm run schema` (which rebuilds plot-schema.json and then runs the schema type generator). ## What's hand-written and stays that way The schema doesn't describe: - **Events** — `PlotMouseEvent`, `PlotHoverEvent`, `LegendClickEvent`, `PlotlyHTMLElement` and its `on()` overloads. These are runtime contracts. - **Public API function signatures** — `Plotly.newPlot`, `relayout`, `restyle`, etc. Live in `src/types/core/api.d.ts`. - **Internal runtime state** — `FullLayout._modules`, `GraphDiv._fullData`, `_calcInverseTransform`, etc. Live in `.internal.d.ts` files in `src/types/core/`. - **Behavioral types** — `ModeBarButton`, `ModeBarDefaultButtons`, `Icon`, `ButtonClickEvent`, `Template`. These describe runtime behavior patterns not captured in the attribute schema. - **Utility types** — `Color`, `Datum`, `TypedArray`, `MarkerSymbol`, etc. Live in `src/types/lib/common.d.ts`. The generator references these types. ## Adding internal properties When converting a JS file to TS and discovering an internal property like `fullLayout._someFlag`, add it to the corresponding `Full*` interface: ```ts // src/types/core/layout.internal.d.ts export interface FullLayout extends Layout { _modules?: any[]; _someFlag?: boolean; // add new ones here [key: string]: any; } ``` The `[key: string]: any` index signature is intentional — it absorbs unknown internal properties so JS code can be migrated piecewise without type errors. ## Coding conventions ### JSDoc on TypeScript functions For exported functions in `.ts` source files, use a top-level JSDoc block with `@param name - description` lines. Omit the type from `@param` (the TS signature already has it) and use a hyphen separator: ```ts /** * make a regex for matching counter ids/names ie xaxis, xaxis2, xaxis10... * * @param head - the head of the pattern, eg 'x' matches 'x', 'x2', 'x10' etc. * @param tail - a fixed piece after the id */ export function counter(head: string, tail: string = '') { ... } ``` ## See also - [SETUP.md](SETUP.md) — toolchain and npm scripts - [CONVERTING_ATTRIBUTES.md](CONVERTING_ATTRIBUTES.md) — the conversion recipe - [GENERATOR.md](GENERATOR.md) — internals of the type generators