diff --git a/draftlogs/8066_change.md b/draftlogs/8066_change.md new file mode 100644 index 00000000000..e7d9ed31ddd --- /dev/null +++ b/draftlogs/8066_change.md @@ -0,0 +1 @@ +- Describe `config.toImageButtonOptions` in the plot schema [[#8066](https://github.com/plotly/plotly.js/pull/8066)] diff --git a/draftlogs/8066_fix.md b/draftlogs/8066_fix.md new file mode 100644 index 00000000000..ca7111e7781 --- /dev/null +++ b/draftlogs/8066_fix.md @@ -0,0 +1 @@ +- Correct the image export types: `format` now accepts `full-json`, `width` and `height` accept `null`, and `ToImgopts.format`, `ToImgopts.width`, `ToImgopts.height` and `DownloadImgopts.filename` are optional because `toImage` and `downloadImage` supply them [[#8066](https://github.com/plotly/plotly.js/pull/8066)] diff --git a/src/plot_api/plot_config.js b/src/plot_api/plot_config.js index dc67084bb35..6d75724da00 100644 --- a/src/plot_api/plot_config.js +++ b/src/plot_api/plot_config.js @@ -12,6 +12,12 @@ * at the moment. */ +const { format, width, height, scale } = require('./to_image_attributes').default; + +// `toImage` owns the scale default, not the button. A default here would reach +// `downloadImage` on every click and change what the caller receives. +const { dflt: scaleDflt, ...buttonScale } = scale; + var configAttributes = { staticPlot: { valType: 'boolean', @@ -50,6 +56,11 @@ var configAttributes = { ].join(' ') }, edits: { + description: [ + 'Determines which parts of the graph a user can edit directly.', + '`editable` sets every key here at once, and a key set here overrides it.', + '`staticPlot` disables all of them.' + ].join(' '), annotationPosition: { valType: 'boolean', dflt: false, @@ -279,13 +290,24 @@ var configAttributes = { ].join(' ') }, toImageButtonOptions: { - valType: 'any', - dflt: {}, description: [ - 'Statically override options for toImage modebar button', - 'allowed keys are format, filename, width, height, scale', - 'see ../components/modebar/buttons.js' - ].join(' ') + 'Statically overrides options for the toImage modebar button.', + 'The button reads only `format`, `filename`, `width`, `height` and `scale`,', + 'and drops every other key.' + ].join(' '), + format, + filename: { + valType: 'string', + description: [ + 'Sets the name of the downloaded file, without an extension.', + 'The button appends the extension that matches `format`.', + 'The name defaults to the plot title, then the plot subtitle,', + 'then *plot-image*.' + ].join(' ') + }, + width, + height, + scale: buttonScale }, displaylogo: { valType: 'boolean', @@ -414,12 +436,17 @@ var configAttributes = { var dfltConfig = {}; function crawl(src, target) { - for(var k in src) { - var obj = src[k]; - if(obj.valType) { - target[k] = obj.dflt; + for (const k in src) { + const obj = src[k]; + // A container carries its own `description` string beside the child + // attributes. Recursing into a string never terminates. + if (typeof obj !== 'object' || obj === null) continue; + if (obj.valType) { + // An attribute without a `dflt` seeds no key. A key holding + // `undefined` reads as present to `in`, which callers test. + if ('dflt' in obj) target[k] = obj.dflt; } else { - if(!target[k]) { + if (!target[k]) { target[k] = {}; } crawl(obj, target[k]); diff --git a/src/plot_api/to_image.js b/src/plot_api/to_image.js index bf6bec98f39..7d67b6f0caf 100644 --- a/src/plot_api/to_image.js +++ b/src/plot_api/to_image.js @@ -10,63 +10,7 @@ var helpers = require('../snapshot/helpers'); var toSVG = require('../snapshot/tosvg'); var svgToImg = require('../snapshot/svgtoimg'); var version = require('../version').version; - -var attrs = { - format: { - valType: 'enumerated', - values: ['png', 'jpeg', 'webp', 'svg', 'full-json'], - dflt: 'png', - description: 'Sets the format of exported image.' - }, - width: { - valType: 'number', - min: 1, - description: [ - 'Sets the exported image width.', - 'Defaults to the value found in `layout.width`', - 'If set to *null*, the exported image width will match the current graph width.' - ].join(' ') - }, - height: { - valType: 'number', - min: 1, - description: [ - 'Sets the exported image height.', - 'Defaults to the value found in `layout.height`', - 'If set to *null*, the exported image height will match the current graph height.' - ].join(' ') - }, - scale: { - valType: 'number', - min: 0, - dflt: 1, - description: [ - 'Sets a scaling for the generated image.', - 'If set, all features of a graphs (e.g. text, line width)', - 'are scaled, unlike simply setting', - 'a bigger *width* and *height*.' - ].join(' ') - }, - setBackground: { - valType: 'any', - dflt: false, - description: [ - 'Sets the image background mode.', - 'By default, the image background is determined by `layout.paper_bgcolor`,', - 'the *transparent* mode.', - 'One might consider setting `setBackground` to *opaque*', - 'when exporting a *jpeg* image as JPEGs do not support opacity.' - ].join(' ') - }, - imageDataOnly: { - valType: 'boolean', - dflt: false, - description: [ - 'Determines whether or not the return value is prefixed by', - 'the image format\'s corresponding \'data:image;\' spec.' - ].join(' ') - } -}; +const attrs = require('./to_image_attributes').default; /** Plotly.toImage * @@ -74,7 +18,7 @@ var attrs = { * can either be a data/layout/config object * or an existing graph
* or an id to an existing graph
- * @param {object} opts (see above) + * @param {object} opts (see ./to_image_attributes) * @return {promise} */ function toImage(gd, opts) { diff --git a/src/plot_api/to_image_attributes.ts b/src/plot_api/to_image_attributes.ts new file mode 100644 index 00000000000..6c61246ac2f --- /dev/null +++ b/src/plot_api/to_image_attributes.ts @@ -0,0 +1,66 @@ +import type { AttributeMap } from '../types/lib/attributes'; + +/** + * Options for `Plotly.toImage` and `Plotly.downloadImage`. + * `plot_config.js` reuses `format.values` for `config.toImageButtonOptions`. + * A separate module prevents the require cycle that `plot_config.js` would + * close through `to_image.js`. + */ +const attributes = { + format: { + valType: 'enumerated', + values: ['png', 'jpeg', 'webp', 'svg', 'full-json'] as const, + dflt: 'png', + description: 'Sets the format of exported image.' + }, + width: { + valType: 'number', + min: 1, + description: [ + 'Sets the exported image width.', + 'Defaults to the value found in `layout.width`', + 'If set to *null*, the exported image width will match the current graph width.' + ].join(' ') + }, + height: { + valType: 'number', + min: 1, + description: [ + 'Sets the exported image height.', + 'Defaults to the value found in `layout.height`', + 'If set to *null*, the exported image height will match the current graph height.' + ].join(' ') + }, + scale: { + valType: 'number', + min: 0, + dflt: 1, + description: [ + 'Sets a scaling for the generated image.', + 'If set, all features of a graphs (e.g. text, line width)', + 'are scaled, unlike simply setting', + 'a bigger *width* and *height*.' + ].join(' ') + }, + setBackground: { + valType: 'any', + dflt: false, + description: [ + 'Sets the image background mode.', + 'By default, the image background is determined by `layout.paper_bgcolor`,', + 'the *transparent* mode.', + 'One might consider setting `setBackground` to *opaque*', + 'when exporting a *jpeg* image as JPEGs do not support opacity.' + ].join(' ') + }, + imageDataOnly: { + valType: 'boolean', + dflt: false, + description: [ + 'Determines whether or not the return value is prefixed by', + "the image format's corresponding 'data:image;' spec." + ].join(' ') + } +} as const satisfies AttributeMap; + +export default attributes; diff --git a/src/types/core/config.d.ts b/src/types/core/config.d.ts index 5d8e57e63c0..f705374e90f 100644 --- a/src/types/core/config.d.ts +++ b/src/types/core/config.d.ts @@ -1,97 +1,80 @@ /** * Config types * - * `Config` is built by overlaying a small hand-written interface on top of - * the schema-derived `ConfigBase`. Most fields come straight from the - * schema; the overrides cover seven fields whose schema `valType` is `any` - * because the underlying JS attribute accepts a function value, an - * arbitrary-key map, or a structure too irregular for the schema to model. + * This file overlays a small hand-written interface on the schema-derived + * `ConfigBase` to build `Config`. Most fields come straight from the schema. + * The overrides cover the five fields whose schema `valType` is `any`. Those + * fields accept a function value, an arbitrary-key map, or a structure too + * irregular for the schema to model. */ -import type { ConfigBase, Edits } from '../generated/schema'; +import type { ConfigBase, Edits, ToImageButtonOptions, ToImageFormat } from '../generated/schema'; import type { PlotlyHTMLElement } from './events'; import type { ModeBarButtonAny, ModeBarDefaultButtons } from './layout'; -export type { Edits }; +export type { Edits, ToImageButtonOptions, ToImageFormat }; // --------------------------------------------------------------------------- // Image export options // --------------------------------------------------------------------------- /** - * Options for `Plotly.toImage`. The graph is rendered to a string suitable - * for use as a data URI or as raw SVG markup. + * Background mode for `Plotly.toImage` and `config.setBackground`. + * A function receives the graph div and the resolved background color. */ -export interface ToImgopts { - /** Output image format. */ - format: 'jpeg' | 'png' | 'webp' | 'svg'; - /** If null, uses current graph width */ - width: number | null; - /** If null, uses current graph height */ - height: number | null; - /** Resolution multiplier for raster formats. */ - scale?: number | undefined; -} +export type SetBackground = 'opaque' | 'transparent' | ((gd: PlotlyHTMLElement, bgColor: string) => void); /** - * Options for `Plotly.downloadImage`. Like `ToImgopts`, but also requires - * a `filename` because the result is saved to disk by the browser. + * Options for `Plotly.toImage`. `toImage` renders the graph to a string that + * works as a data URI or as raw SVG markup. The mode-bar button reads the same + * fields, minus the two below, through `config.toImageButtonOptions`. */ -export interface DownloadImgopts { - /** Output image format. */ - format: 'jpeg' | 'png' | 'webp' | 'svg'; - /** Output width in pixels. */ - width: number | null; - /** Output height in pixels. */ - height: number | null; - /** Filename used for the downloaded file (no extension required). */ - filename: string; +export interface ToImgopts extends Omit { + /** Override the background color with a static color name, or with a function that runs on each render */ + setBackground?: SetBackground; + /** Return the bare image data, without the leading `data:image;` prefix */ + imageDataOnly?: boolean; } /** - * Static defaults applied to the mode-bar "download image" button. Set - * via `config.toImageButtonOptions`. + * Options for `Plotly.downloadImage`. Like `ToImgopts`, but adds a `filename` + * because the browser saves the result to disk. `downloadImage` forces + * `imageDataOnly` on, so a caller cannot set it. */ -export interface ToImageButtonOptions { - /** Output image format. */ - format?: 'png' | 'svg' | 'jpeg' | 'webp'; - /** Downloaded filename. */ +export interface DownloadImgopts extends Omit { + /** + * Name for the downloaded file, without an extension. `downloadImage` + * appends the extension that matches `format`. The name defaults to the + * plot title, then the plot subtitle, then `plot-image`. + */ filename?: string; - /** Output height in pixels. */ - height?: number; - /** Output width in pixels. */ - width?: number; - /** Resolution multiplier for raster formats. */ - scale?: number; } // --------------------------------------------------------------------------- -// Config — hybrid (schema-derived + hand-written overrides) +// Config - hybrid (schema-derived + hand-written overrides) // --------------------------------------------------------------------------- /** - * Hand-written overrides for the six `schema.config` fields whose - * `valType` is `any`. These accept functions or arbitrary-key maps that the - * JSON schema fundamentally cannot describe, so they stay typed by hand. + * Hand-written overrides for the five `schema.config` fields whose `valType` + * is `any`. These fields accept functions or arbitrary-key maps, which the + * JSON schema cannot describe. */ interface ConfigOverrides { - /** Override the background color: a static color name, or a function called per-render. */ - setBackground?: 'opaque' | 'transparent' | ((gd: PlotlyHTMLElement, bgColor: string) => void); - /** Define fully custom mode bar buttons as nested array of button groups. */ + /** Override the background color with a static color name, or with a function that runs on each render */ + setBackground?: SetBackground; + /** Define fully custom mode bar buttons as a nested array of button groups */ modeBarButtons?: ModeBarButtonAny[][] | false; - /** Add mode bar buttons using config objects or default-button names. */ + /** Add mode bar buttons with config objects or default-button names */ modeBarButtonsToAdd?: ModeBarButtonAny[]; - /** Remove mode bar buttons by name. */ + /** Remove mode bar buttons by name */ modeBarButtonsToRemove?: ModeBarDefaultButtons[]; - /** Statically override options for the toImage mode bar button. */ - toImageButtonOptions?: ToImageButtonOptions; - /** Localization definitions keyed by locale id (e.g. `'en-US'`, `'fr'`). */ + /** Localization definitions under a locale id key, for example `'en-US'` or `'fr'` */ locales?: Record; format?: Record }>; } /** - * Full plot config. Combines `ConfigBase` (schema-derived) with the - * hand-written `ConfigOverrides` so the hand-written entries replace the - * loosely-typed `any` versions from the schema. + * Full plot config. `Config` combines `ConfigBase` (schema-derived) with the + * hand-written `ConfigOverrides`, so the hand-written entries replace the + * loosely-typed versions from the schema. */ export type Config = Omit & ConfigOverrides; diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index fda9a6817e9..5283911ff43 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -32,6 +32,8 @@ export type PatternShape = '' | '/' | '\\' | 'x' | '-' | '|' | '+' | '.'; export type TransitionEasing = 'linear' | 'quad' | 'cubic' | 'sin' | 'exp' | 'circle' | 'elastic' | 'back' | 'bounce' | 'linear-in' | 'quad-in' | 'cubic-in' | 'sin-in' | 'exp-in' | 'circle-in' | 'elastic-in' | 'back-in' | 'bounce-in' | 'linear-out' | 'quad-out' | 'cubic-out' | 'sin-out' | 'exp-out' | 'circle-out' | 'elastic-out' | 'back-out' | 'bounce-out' | 'linear-in-out' | 'quad-in-out' | 'cubic-in-out' | 'sin-in-out' | 'exp-in-out' | 'circle-in-out' | 'elastic-in-out' | 'back-in-out' | 'bounce-in-out'; +export type ToImageFormat = 'png' | 'jpeg' | 'webp' | 'svg' | 'full-json'; + export type TraceType = 'bar' | 'barpolar' | 'box' | 'candlestick' | 'carpet' | 'choropleth' | 'choroplethmap' | 'cone' | 'contour' | 'contourcarpet' | 'densitymap' | 'funnel' | 'funnelarea' | 'heatmap' | 'histogram' | 'histogram2d' | 'histogram2dcontour' | 'icicle' | 'image' | 'indicator' | 'isosurface' | 'mesh3d' | 'ohlc' | 'parcats' | 'parcoords' | 'pie' | 'quiver' | 'sankey' | 'scatter' | 'scatter3d' | 'scattercarpet' | 'scattergeo' | 'scattergl' | 'scattermap' | 'scatterpolar' | 'scatterpolargl' | 'scattersmith' | 'scatterternary' | 'splom' | 'streamtube' | 'sunburst' | 'surface' | 'table' | 'treemap' | 'violin' | 'volume' | 'waterfall'; /** @deprecated Renamed to TraceType. */ @@ -16700,6 +16702,31 @@ export interface Edits { titleText?: boolean; } +export interface ToImageButtonOptions { + /** Sets the name of the downloaded file, without an extension. The button appends the extension that matches `format`. The name defaults to the plot title, then the plot subtitle, then *plot-image*. */ + filename?: string; + /** + * Sets the format of exported image. + * @default 'png' + */ + format?: ToImageFormat; + /** + * Sets the exported image height. Defaults to the value found in `layout.height` If set to *null*, the exported image height will match the current graph height. + * Minimum: 1 + */ + height?: number | null; + /** + * Sets a scaling for the generated image. If set, all features of a graphs (e.g. text, line width) are scaled, unlike simply setting a bigger *width* and *height*. + * Minimum: 0 + */ + scale?: number; + /** + * Sets the exported image width. Defaults to the value found in `layout.width` If set to *null*, the exported image width will match the current graph width. + * Minimum: 1 + */ + width?: number | null; +} + export interface ConfigBase { /** * Determines whether the graphs are plotted with respect to layout.autosize:true and infer its container size. @@ -16742,6 +16769,7 @@ export interface ConfigBase { * @default false */ editable?: boolean; + /** Determines which parts of the graph a user can edit directly. `editable` sets every key here at once, and a key set here overrides it. `staticPlot` disables all of them. */ edits?: Edits; /** * When `layout.autosize` is turned on, determines whether the graph fills the container (the default) or the screen (if set to *true*). @@ -16848,11 +16876,8 @@ export interface ConfigBase { * @default false */ staticPlot?: boolean; - /** - * Statically override options for toImage modebar button allowed keys are format, filename, width, height, scale see ../components/modebar/buttons.js - * @default {} - */ - toImageButtonOptions?: any; + /** Statically overrides options for the toImage modebar button. The button reads only `format`, `filename`, `width`, `height` and `scale`, and drops every other key. */ + toImageButtonOptions?: ToImageButtonOptions; /** * Set the URL to topojson used in geo charts. By default, the topojson files are fetched from cdn.plot.ly. For example, set this option to: /dist/topojson/ to render geographical feature using the topojson files that ship with the plotly.js module. * @default 'https://cdn.plot.ly/un/' diff --git a/tasks/generate_schema_types.mjs b/tasks/generate_schema_types.mjs index 01c9ed5961d..ce462bb7fdf 100644 --- a/tasks/generate_schema_types.mjs +++ b/tasks/generate_schema_types.mjs @@ -48,7 +48,8 @@ const COMMON_TYPE_ANCHORS = [ match: (key, _path, values) => key === 'yref' && values.length === 2 && values.includes('container') }, { name: 'PatternShape', match: (key, path) => key === 'shape' && /\.pattern\.shape$/.test(path) }, - { name: 'TransitionEasing', match: (key) => key === 'easing' } + { name: 'TransitionEasing', match: (key) => key === 'easing' }, + { name: 'ToImageFormat', match: (_key, path) => path === 'config.toImageButtonOptions.format' } ]; /** @@ -116,6 +117,7 @@ function discoverCommonTypes(schema) { visit(schema.traces, 'traces'); visit(schema.layout, 'layout'); if (schema.animation) visit(schema.animation, 'animation'); + if (schema.config) visit(schema.config, 'config'); // For each anchor, pick the largest value set. When sizes tie, the first // match wins. The superset rule handles axis-type-style cases where @@ -1389,14 +1391,30 @@ export function generateSchemaTypes(schema, outputPath) { // references `edits?: Edits` instead of re-inlining the subtree. sharedTypes.set(containerFingerprint(schema.config.edits), 'Edits'); } + if (schema.config && schema.config.toImageButtonOptions) { + // The runtime reads `null` for `width` and `height` as the current graph + // size. A `number` valType cannot express that, so widen the two fields + // here rather than in `core/config.d.ts`. + const toImageButtonProps = attrsToProperties( + schema.config.toImageButtonOptions, + ' ', + 'toImageButtonOptions', + sharedTypes, + { width: 'number | null', height: 'number | null' } + ); + extraInterfaces.push({ name: 'ToImageButtonOptions', properties: toImageButtonProps }); + + // Register the fingerprint so the ConfigBase emission below references + // `toImageButtonOptions?: ToImageButtonOptions` instead of re-inlining the subtree. + sharedTypes.set(containerFingerprint(schema.config.toImageButtonOptions), 'ToImageButtonOptions'); + } if (schema.config) { // Generate the schema-derived Config building block. Fields whose - // schema valType is `any` (locales, modeBarButtons, setBackground, - // toImageButtonOptions, ...) come through as `any` and are - // overridden with precise types in `core/config.d.ts`'s `Config` - // via Omit/intersection. The schema is fundamentally unable to - // describe functions or arbitrary-key maps, so those overrides are - // permanent. + // schema valType is `any` (locales, modeBarButtons, setBackground, ...) + // come through as `any` and are overridden with precise types in + // `core/config.d.ts`'s `Config` via Omit/intersection. The schema is + // fundamentally unable to describe functions or arbitrary-key maps, so + // those overrides are permanent. const configProps = attrsToProperties(schema.config, ' ', 'config', sharedTypes); extraInterfaces.push({ name: 'ConfigBase', properties: configProps }); } diff --git a/test/jasmine/bundle_tests/plotschema_test.js b/test/jasmine/bundle_tests/plotschema_test.js index ac5301316cf..1496bf3cb4b 100644 --- a/test/jasmine/bundle_tests/plotschema_test.js +++ b/test/jasmine/bundle_tests/plotschema_test.js @@ -291,6 +291,14 @@ describe('plot schema', function() { expect(plotSchema.config.scrollZoom).toBeDefined(); }); + it('should describe every toImageButtonOptions key', () => { + const opts = plotSchema.config.toImageButtonOptions; + expect(opts.role).toBe('object'); + expect(Object.keys(opts).sort()).toEqual(['description', 'filename', 'format', 'height', 'role', 'scale', 'width']); + expect(opts.format.values).toEqual(['png', 'jpeg', 'webp', 'svg', 'full-json']); + expect(opts.format.dflt).toBe('png'); + }); + it('should list trace-dependent & direction-dependent error bar attributes', function() { var scatterSchema = plotSchema.traces.scatter.attributes; expect(scatterSchema.error_x.copy_ystyle).toBeDefined(); diff --git a/test/plot-schema.json b/test/plot-schema.json index cebf8f090da..3f8728d1b01 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -182,6 +182,7 @@ "dflt": false, "valType": "boolean" }, + "description": "Determines which parts of the graph a user can edit directly. `editable` sets every key here at once, and a key set here overrides it. `staticPlot` disables all of them.", "legendPosition": { "description": "Enables moving the legend.", "dflt": false, @@ -329,9 +330,39 @@ "valType": "boolean" }, "toImageButtonOptions": { - "description": "Statically override options for toImage modebar button allowed keys are format, filename, width, height, scale see ../components/modebar/buttons.js", - "dflt": {}, - "valType": "any" + "description": "Statically overrides options for the toImage modebar button. The button reads only `format`, `filename`, `width`, `height` and `scale`, and drops every other key.", + "filename": { + "description": "Sets the name of the downloaded file, without an extension. The button appends the extension that matches `format`. The name defaults to the plot title, then the plot subtitle, then *plot-image*.", + "valType": "string" + }, + "format": { + "description": "Sets the format of exported image.", + "dflt": "png", + "valType": "enumerated", + "values": [ + "png", + "jpeg", + "webp", + "svg", + "full-json" + ] + }, + "height": { + "description": "Sets the exported image height. Defaults to the value found in `layout.height` If set to *null*, the exported image height will match the current graph height.", + "min": 1, + "valType": "number" + }, + "role": "object", + "scale": { + "description": "Sets a scaling for the generated image. If set, all features of a graphs (e.g. text, line width) are scaled, unlike simply setting a bigger *width* and *height*.", + "min": 0, + "valType": "number" + }, + "width": { + "description": "Sets the exported image width. Defaults to the value found in `layout.width` If set to *null*, the exported image width will match the current graph width.", + "min": 1, + "valType": "number" + } }, "topojsonURL": { "description": "Set the URL to topojson used in geo charts. By default, the topojson files are fetched from cdn.plot.ly. For example, set this option to: /dist/topojson/ to render geographical feature using the topojson files that ship with the plotly.js module.",