diff --git a/draftlogs/8020_fix.md b/draftlogs/8020_fix.md new file mode 100644 index 00000000000..0fc60592a10 --- /dev/null +++ b/draftlogs/8020_fix.md @@ -0,0 +1 @@ +- Include trace-contributed layout attributes in generated types [[#8020](https://github.com/plotly/plotly.js/pull/8020)] diff --git a/src/types/GENERATOR.md b/src/types/GENERATOR.md index 49f2a9973c6..78350204dc5 100644 --- a/src/types/GENERATOR.md +++ b/src/types/GENERATOR.md @@ -99,7 +99,29 @@ schema flows through automatically. ### Phase 4: Layout types -Layout generation handles three categories: +Before generation, `mergeTraceLayoutAttributes` folds every +`schema.traces[].layoutAttributes` map into the layout attribute tree. +Trace modules contribute layout keys such as `barmode`, `boxmode` and +`piecolorway`, and the schema files them under the trace that contributes +them. At runtime they are ordinary layout keys, so the generated types must +carry them. + +Most modules contribute to the top level of layout. barpolar is the +exception: its `supplyLayoutDefaults` coerces from `layoutIn[trace.subplot]`, +so `layout.polar.barmode` is the real key and `layout.barmode` does nothing +for a barpolar trace. `SUBPLOT_SCOPED_TRACE_LAYOUT` maps barpolar to `polar`, +which is why `Layout.barmode` allows all four bar values while +`PolarLayout.barmode` allows only `'stack'` and `'overlay'`. Add an entry +there if another module ever reads its layout attributes from a subplot. + +Two modules that contribute the same key to the same target must agree on +the definition. If they disagree, generation throws rather than guessing, +because one property cannot describe both. The repo shares such keys by +reference instead of copying them, so a disagreement means either the shared +require was broken, or the two modules mean genuinely different things and +one of them belongs in its own container. + +Layout generation then handles three categories: - **Subplot containers** (`_isSubplotObj` flag) — grouped by target name and merged into supersets. E.g., `xaxis` and `yaxis` both map to diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index f1d8a0d0f72..7948e3c25c6 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -13177,6 +13177,17 @@ export interface PolarLayout { */ visible?: boolean; }; + /** + * Sets the gap between bars of adjacent location coordinates. Values are unitless, they represent fractions of the minimum difference in bar positions in the data. + * @default 0.1 + * Range: [0, 1] + */ + bargap?: number; + /** + * Determines how bars at the same location coordinate are displayed on the graph. With *stack*, the bars are stacked on top of one another With *overlay*, the bars are plotted over one another, you might need to reduce *opacity* to see multiple bars. + * @default 'stack' + */ + barmode?: 'stack' | 'overlay'; /** * Set the background color of the subplot * @default '#fff' @@ -15939,6 +15950,43 @@ export interface Layout { * @default 'convert types' */ autotypenumbers?: 'convert types' | 'strict'; + /** Sets the rounding of bar corners. May be an integer number of pixels, or a percentage of bar width (as a string ending in %). */ + barcornerradius?: any; + /** + * Sets the gap (in plot fraction) between bars of adjacent location coordinates. + * Range: [0, 1] + */ + bargap?: number; + /** + * Sets the gap (in plot fraction) between bars of the same location coordinate. + * @default 0 + * Range: [0, 1] + */ + bargroupgap?: number; + /** + * Determines how bars at the same location coordinate are displayed on the graph. With *stack*, the bars are stacked on top of one another With *relative*, the bars are stacked on top of one another, with negative values below the axis, positive values above With *group*, the bars are plotted next to one another centered around the shared location. With *overlay*, the bars are plotted over one another, you might need to reduce *opacity* to see multiple bars. + * @default 'group' + */ + barmode?: 'stack' | 'group' | 'overlay' | 'relative'; + /** Sets the normalization for bar traces on the graph. With *fraction*, the value of each bar is divided by the sum of all values at that location coordinate. *percent* is the same but multiplied by 100 to show percentages. */ + barnorm?: '' | 'fraction' | 'percent'; + /** + * Sets the gap (in plot fraction) between boxes of adjacent location coordinates. Has no effect on traces that have *width* set. + * @default 0.3 + * Range: [0, 1] + */ + boxgap?: number; + /** + * Sets the gap (in plot fraction) between boxes of the same location coordinate. Has no effect on traces that have *width* set. + * @default 0.3 + * Range: [0, 1] + */ + boxgroupgap?: number; + /** + * Determines how boxes at the same location coordinate are displayed on the graph. If *group*, the boxes are plotted next to one another centered around the shared location. If *overlay*, the boxes are plotted over one another, you might need to set *opacity* to see them multiple boxes. Has no effect on traces that have *width* set. + * @default 'overlay' + */ + boxmode?: 'group' | 'overlay'; /** * Sets the default calendar system to use for interpreting and displaying dates throughout the plot. * @default 'gregorian' @@ -15988,8 +16036,51 @@ export interface Layout { dragmode?: 'zoom' | 'pan' | 'select' | 'lasso' | 'drawclosedpath' | 'drawopenpath' | 'drawline' | 'drawrect' | 'drawcircle' | 'orbit' | 'turntable' | false; /** Controls persistence of user-driven changes in `editable: true` configuration, other than trace names and axis titles. Defaults to `layout.uirevision`. */ editrevision?: any; + /** + * If `true`, the funnelarea slice colors (whether given by `funnelareacolorway` or inherited from `colorway`) will be extended to three times its original length by first repeating every color 20% lighter then each color 20% darker. This is intended to reduce the likelihood of reusing the same color when you have many slices, but you can set `false` to disable. Colors provided in the trace, using `marker.colors`, are never extended. + * @default true + */ + extendfunnelareacolors?: boolean; + /** + * If `true`, the icicle slice colors (whether given by `iciclecolorway` or inherited from `colorway`) will be extended to three times its original length by first repeating every color 20% lighter then each color 20% darker. This is intended to reduce the likelihood of reusing the same color when you have many slices, but you can set `false` to disable. Colors provided in the trace, using `marker.colors`, are never extended. + * @default true + */ + extendiciclecolors?: boolean; + /** + * If `true`, the pie slice colors (whether given by `piecolorway` or inherited from `colorway`) will be extended to three times its original length by first repeating every color 20% lighter then each color 20% darker. This is intended to reduce the likelihood of reusing the same color when you have many slices, but you can set `false` to disable. Colors provided in the trace, using `marker.colors`, are never extended. + * @default true + */ + extendpiecolors?: boolean; + /** + * If `true`, the sunburst slice colors (whether given by `sunburstcolorway` or inherited from `colorway`) will be extended to three times its original length by first repeating every color 20% lighter then each color 20% darker. This is intended to reduce the likelihood of reusing the same color when you have many slices, but you can set `false` to disable. Colors provided in the trace, using `marker.colors`, are never extended. + * @default true + */ + extendsunburstcolors?: boolean; + /** + * If `true`, the treemap slice colors (whether given by `treemapcolorway` or inherited from `colorway`) will be extended to three times its original length by first repeating every color 20% lighter then each color 20% darker. This is intended to reduce the likelihood of reusing the same color when you have many slices, but you can set `false` to disable. Colors provided in the trace, using `marker.colors`, are never extended. + * @default true + */ + extendtreemapcolors?: boolean; /** Sets the global font. Note that fonts used in traces and other layout components inherit from the global font. */ font?: Font; + /** Sets the default funnelarea slice colors. Defaults to the main `colorway` used for trace colors. If you specify a new list here it can still be extended with lighter and darker colors, see `extendfunnelareacolors`. */ + funnelareacolorway?: Color[]; + /** + * Sets the gap (in plot fraction) between bars of adjacent location coordinates. + * Range: [0, 1] + */ + funnelgap?: number; + /** + * Sets the gap (in plot fraction) between bars of the same location coordinate. + * @default 0 + * Range: [0, 1] + */ + funnelgroupgap?: number; + /** + * Determines how bars at the same location coordinate are displayed on the graph. With *stack*, the bars are stacked on top of one another With *group*, the bars are plotted next to one another centered around the shared location. With *overlay*, the bars are plotted over one another, you might need to reduce *opacity* to see multiple bars. + * @default 'stack' + */ + funnelmode?: 'stack' | 'group' | 'overlay'; geo?: GeoLayout; grid?: { /** @@ -16057,6 +16148,8 @@ export interface Layout { * Minimum: 10 */ height?: number; + /** hiddenlabels is the funnelarea & pie chart analog of visible:'legendonly' but it can contain many labels, and can simultaneously hide slices from several pies/funnelarea charts */ + hiddenlabels?: Datum[] | Datum[][] | TypedArray; /** * If true, `plotly_hover` events will fire for any cursor position within the plot area, not just over traces. When the cursor is not over a trace, the event will have an empty `points` array but will include `xvals` and `yvals` with cursor coordinates in data space, and `xPixel` and `yPixel` with cursor coordinates in pixels, relative to the top-left corner of the graph div. A `plotly_unhover` event fires when the cursor leaves the plot area. * @default false @@ -16109,6 +16202,8 @@ export interface Layout { * @default 'overlaying' */ hoversubplots?: 'single' | 'overlaying' | 'axis'; + /** Sets the default icicle slice colors. Defaults to the main `colorway` used for trace colors. If you specify a new list here it can still be extended with lighter and darker colors, see `extendiciclecolors`. */ + iciclecolorway?: Color[]; images?: LayoutImage[]; legend?: Legend; map?: MapLayout; @@ -16315,12 +16410,24 @@ export interface Layout { * @default '#fff' */ paper_bgcolor?: Color; + /** Sets the default pie slice colors. Defaults to the main `colorway` used for trace colors. If you specify a new list here it can still be extended with lighter and darker colors, see `extendpiecolors`. */ + piecolorway?: Color[]; /** * Sets the background color of the plotting area in-between x and y axes. * @default '#fff' */ plot_bgcolor?: Color; polar?: PolarLayout; + /** + * Sets the gap (in plot fraction) between scatter points of adjacent location coordinates. Defaults to `bargap`. + * Range: [0, 1] + */ + scattergap?: number; + /** + * Determines how scatter points at the same location coordinate are displayed on the graph. With *group*, the scatter points are plotted next to one another centered around the shared location. With *overlay*, the scatter points are plotted over one another, you might need to reduce *opacity* to see multiple scatter points. + * @default 'overlay' + */ + scattermode?: 'group' | 'overlay'; scene?: Scene; /** * When `dragmode` is set to *select*, this limits the selection of the drag to horizontal, vertical or diagonal. *h* only allows horizontal selection, *v* only vertical, *d* only diagonal and *any* sets no limit. @@ -16343,6 +16450,8 @@ export interface Layout { * Minimum: -1 */ spikedistance?: number; + /** Sets the default sunburst slice colors. Defaults to the main `colorway` used for trace colors. If you specify a new list here it can still be extended with lighter and darker colors, see `extendsunburstcolors`. */ + sunburstcolorway?: Color[]; /** Default attributes to be applied to the plot. Templates can be created from existing plots using `Plotly.makeTemplate`, or created manually. They should be objects with format: `{layout: layoutTemplate, data: {[type]: [traceTemplate, ...]}, ...}` `layoutTemplate` and `traceTemplate` are objects matching the attribute structure of `layout` and a data trace. Trace templates are applied cyclically to traces of each type. Container arrays (eg `annotations`) have special handling: An object ending in `defaults` (eg `annotationdefaults`) is applied to each array item. But if an item has a `templateitemname` key we look in the template array for an item with matching `name` and apply that instead. If no matching `name` is found we mark the item invisible. Any named template item not referenced is appended to the end of the array, so you can use this for a watermark annotation or a logo image, for example. To omit one of these items on the plot, make an item with matching `templateitemname` and `visible: false`. */ template?: any; ternary?: TernaryLayout; @@ -16415,6 +16524,8 @@ export interface Layout { }; /** Sets transition options used during Plotly.react updates. */ transition?: Transition; + /** Sets the default treemap slice colors. Defaults to the main `colorway` used for trace colors. If you specify a new list here it can still be extended with lighter and darker colors, see `extendtreemapcolors`. */ + treemapcolorway?: Color[]; /** Used to allow user interactions with the plot to persist after `Plotly.react` calls that are unaware of these interactions. If `uirevision` is omitted, or if it is given and it changed from the previous `Plotly.react` call, the exact new figure is used. If `uirevision` is truthy and did NOT change, any attribute that has been affected by user interactions and did not receive a different value in the new figure will keep the interaction value. `layout.uirevision` attribute serves as the default for `uirevision` attributes in various sub-containers. For finer control you can set these sub-attributes directly. For example, if your app separately controls the data on the x and y axes you might set `xaxis.uirevision=*time*` and `yaxis.uirevision=*cost*`. Then if only the y data is changed, you can update `yaxis.uirevision=*quantity*` and the y axis range will reset but the x axis range will retain any user-driven zoom. */ uirevision?: any; uniformtext?: { @@ -16431,6 +16542,39 @@ export interface Layout { mode?: false | 'hide' | 'show'; }; updatemenus?: UpdateMenu[]; + /** + * Sets the gap (in plot fraction) between violins of adjacent location coordinates. Has no effect on traces that have *width* set. + * @default 0.3 + * Range: [0, 1] + */ + violingap?: number; + /** + * Sets the gap (in plot fraction) between violins of the same location coordinate. Has no effect on traces that have *width* set. + * @default 0.3 + * Range: [0, 1] + */ + violingroupgap?: number; + /** + * Determines how violins at the same location coordinate are displayed on the graph. If *group*, the violins are plotted next to one another centered around the shared location. If *overlay*, the violins are plotted over one another, you might need to set *opacity* to see them multiple violins. Has no effect on traces that have *width* set. + * @default 'overlay' + */ + violinmode?: 'group' | 'overlay'; + /** + * Sets the gap (in plot fraction) between bars of adjacent location coordinates. + * Range: [0, 1] + */ + waterfallgap?: number; + /** + * Sets the gap (in plot fraction) between bars of the same location coordinate. + * @default 0 + * Range: [0, 1] + */ + waterfallgroupgap?: number; + /** + * Determines how bars at the same location coordinate are displayed on the graph. With *group*, the bars are plotted next to one another centered around the shared location. With *overlay*, the bars are plotted over one another, you might need to reduce *opacity* to see multiple bars. + * @default 'group' + */ + waterfallmode?: 'group' | 'overlay'; /** * Sets the plot's width (in px). * @default 700 diff --git a/tasks/generate_schema_types.mjs b/tasks/generate_schema_types.mjs index 81ea20edad9..dbdf59f8388 100644 --- a/tasks/generate_schema_types.mjs +++ b/tasks/generate_schema_types.mjs @@ -1,7 +1,7 @@ /** * Generate TypeScript interfaces from the plot schema. * - * Walks schema.traces and schema.layout.layoutAttributes, emitting interfaces + * Walks schema.traces and the layout attributes, emitting interfaces * for all trace types, layout component types, and a top-level Layout interface * into a single .d.ts file. All attribute metadata (valType, values, arrayOk, * etc.) is mapped to the corresponding TypeScript type. @@ -818,7 +818,7 @@ function traceNameToInterfaceName(traceName) { * attributes into a superset. E.g. xaxis and yaxis both map to LayoutAxis; * their attributes are merged so the interface covers both. * - * @param {object} layoutAttrs - schema.layout.layoutAttributes + * @param {object} layoutAttrs - Layout attributes, as from mergeTraceLayoutAttributes * @returns {Map} */ function buildSubplotGroups(layoutAttrs) { @@ -840,11 +840,113 @@ function buildSubplotGroups(layoutAttrs) { return groups; } +/** + * Trace modules whose layout attributes live inside a subplot container + * instead of at the top level of layout. + * + * The plot schema does not record where a trace module reads its layout + * attributes from, so the mapping lives here. barpolar is the only module + * that reads them from a subplot: its `supplyLayoutDefaults` coerces from + * `layoutIn[trace.subplot]`, which makes `layout.polar.barmode` the real key + * and `layout.barmode` a no-op for barpolar traces. + * + * If a trace module ever reads its layout attributes from another subplot + * container, add it here. Otherwise its keys land on `Layout`, where setting + * them does nothing. + */ +const SUBPLOT_SCOPED_TRACE_LAYOUT = new Map([['barpolar', 'polar']]); + +/** + * Merge the layout attributes that trace modules contribute into the layout + * attribute tree. + * + * The plot schema files these under `schema.traces[].layoutAttributes` + * because a trace module only contributes them when the module loads. At + * runtime they are ordinary layout keys, so the generated types must carry + * them. Without this merge, `Layout` omits `barmode`, `boxmode`, + * `piecolorway` and 25 other keys, and `PolarLayout` omits `barmode` and + * `bargap`. + * + * Keys from a module in `SUBPLOT_SCOPED_TRACE_LAYOUT` merge into that subplot + * container. All other keys merge into the top level. + * + * A key that the layout already defines keeps the layout definition. Two + * modules that contribute the same key to the same target must agree on the + * definition, otherwise one interface cannot describe both and this throws. + * + * The merge copies rather than mutates, because `generateSchemaTypes` is + * exported and must not rewrite a caller's schema object. Only the top level + * and each subplot container this writes to need a copy. Attribute objects + * below those two levels stay shared by reference, and nothing here writes + * to them. + * + * @param {object} schema - The full plot schema + * @returns {object} A new attribute map. The input schema is not modified. + */ +function mergeTraceLayoutAttributes(schema) { + const layoutAttrs = schema.layout.layoutAttributes; + // Target container name ('' for the top level) → key → {attr, traceName}. + const contributed = new Map(); + + for (const traceName of Object.keys(schema.traces).sort()) { + const traceLayoutAttrs = schema.traces[traceName].layoutAttributes; + if (!traceLayoutAttrs || typeof traceLayoutAttrs !== 'object') continue; + + const container = SUBPLOT_SCOPED_TRACE_LAYOUT.get(traceName) || ''; + const target = container ? layoutAttrs[container] : layoutAttrs; + if (!target) { + throw new Error( + `Trace '${traceName}' is mapped to the layout container '${container}', ` + + `but the schema has no 'layout.layoutAttributes.${container}'. ` + + `Update SUBPLOT_SCOPED_TRACE_LAYOUT.` + ); + } + + if (!contributed.has(container)) contributed.set(container, new Map()); + const claimed = contributed.get(container); + + for (const [key, val] of Object.entries(traceLayoutAttrs)) { + if (key in target) continue; + if (!val || typeof val !== 'object') continue; + + const existing = claimed.get(key); + if (!existing) { + claimed.set(key, { attr: val, traceName }); + continue; + } + // Compare the TypeScript type each definition would emit, not the + // raw schema entry: a differing `description` or `dflt` only + // changes the JSDoc, and the first contributor's wins. + const emitted = (attr) => (attr.valType ? leafFingerprint(attr, key) : containerFingerprint(attr)); + if (emitted(existing.attr) !== emitted(val)) { + const where = container ? `layout.${container}` : 'layout'; + throw new Error( + `Traces '${existing.traceName}' and '${traceName}' both contribute '${key}' to ${where}, ` + + `but their definitions differ, so one generated property cannot describe both. ` + + `If the two mean the same thing, share a single definition by reference.` + + `If they mean different things, add the trace to SUBPLOT_SCOPED_TRACE_LAYOUT.` + ); + } + } + } + + const merged = { ...layoutAttrs }; + for (const [container, claimed] of contributed) { + const additions = Object.fromEntries([...claimed].map(([key, { attr }]) => [key, attr])); + if (container) { + merged[container] = { ...merged[container], ...additions }; + } else { + Object.assign(merged, additions); + } + } + return merged; +} + /** * Extract linked-to-array containers from layout attributes. * Returns a map of interface name → item attributes. * - * @param {object} layoutAttrs - schema.layout.layoutAttributes + * @param {object} layoutAttrs - Layout attributes, as from mergeTraceLayoutAttributes * @returns {Map} name → item child attrs */ function extractArrayItems(layoutAttrs) { @@ -862,7 +964,7 @@ function extractArrayItems(layoutAttrs) { /** * Generate the Layout interface body. * - * @param {object} layoutAttrs - schema.layout.layoutAttributes + * @param {object} layoutAttrs - Layout attributes, as from mergeTraceLayoutAttributes * @param {Map} sharedTypes - fingerprint → interface name * @param {Map} subplotGroups - from buildSubplotGroups * @param {Map} arrayItems - from extractArrayItems @@ -965,7 +1067,6 @@ function generateLayoutProperties(layoutAttrs, sharedTypes, subplotGroups, array */ export function generateSchemaTypes(schema, outputPath) { const traceNames = Object.keys(schema.traces).sort(); - const layoutAttrs = schema.layout.layoutAttributes; // Populate META_KEYS from the schema's own list of metadata keys so we // pick up any future additions without code changes. @@ -978,6 +1079,8 @@ export function generateSchemaTypes(schema, outputPath) { // ----- Phase 0: Discover common enum types (Calendar, Dash, ...) ----- const commonTypes = discoverCommonTypes(schema); + const layoutAttrs = mergeTraceLayoutAttributes(schema); + // ----- Phase 1: Fingerprint all container subtrees across traces + layout ----- const collector = new Map(); for (const traceName of traceNames) {