@danielsimonjr/sciengjs 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ sciengjs turns diagram source into images on your own machine. It sends no sourc
10
10
 
11
11
  ## What this release does
12
12
 
13
- Release 0.4.0 holds four input types: **Graphviz DOT**, **Mermaid** (every diagram type), **LaTeX math** (to SVG, PNG and PDF) and **HTML** (to PNG and PDF). The browser stage draws the Mermaid types other than class, state and sequence, and every PDF.
13
+ Release 0.5.0 holds five input types: **Graphviz DOT**, **Mermaid** (every diagram type), **LaTeX math** (to SVG, PNG and PDF), **Plotly plots** (to SVG, PNG and HTML) and **HTML** (to PNG and PDF). The browser stage draws the Mermaid types other than class, state and sequence, and every PDF.
14
14
 
15
15
  | Input | Output | Engine |
16
16
  |---|---|---|
@@ -170,6 +170,107 @@ const svg = await render({ type: "math", source: "\\frac{a}{b}", format: "svg" }
170
170
  const png = await render({ type: "math", source: "\\ce{H2O + CO2 -> H2CO3}", format: "png", options: { scale: 2 } });
171
171
  ```
172
172
 
173
+ ## Plots
174
+
175
+ The `plot` input draws a chart from JSON. The source is a JSON text with the keys `data` and `layout`. The output formats are `svg`, `png` and `html`. The `svg` and `png` outputs make no network request.
176
+
177
+ ```js
178
+ import { render } from "@danielsimonjr/sciengjs";
179
+
180
+ const source = JSON.stringify({
181
+ data: [{ type: "scatter", mode: "lines+markers", x: [0, 1, 2], y: [0, 1, 4], name: "y = x^2" }],
182
+ layout: { title: { text: "Square" } },
183
+ });
184
+ const { bytes } = await render({ type: "plot", source, format: "svg" }, {});
185
+ ```
186
+
187
+ The engine reads a subset of the Plotly figure format. The subset holds `scatter` traces, page and grid layout keys, `line` and `rect` shapes, and linear or log axes. A key outside the subset rejects with `E_INPUT` and the key path. Set `lenient` to `true` to skip such a key and get a warning. The `html` output is a page that draws the figure with Plotly 4.2.0, the pinned build. The page loads that build from a CDN, with a subresource integrity hash. The package does not ship Plotly. The full subset is in the [design](docs/superpowers/specs/2026-11-01-scieng-engine-design.md), section PLOT.
188
+
189
+ The reader applies these rules:
190
+
191
+ - A figure has the top-level keys `data` and `layout` only.
192
+ - The first axis of each letter is `xaxis` or `yaxis`. The other axes are `xaxis2`, `xaxis3` and up, with no leading zero. Plotly also accepts `xaxis1` for the first axis. This reader rejects `xaxis1`, so that each axis has one name.
193
+ - An axis key must name an axis of a subplot in `layout.grid`.
194
+ - `tickformat` is `.Nf` only, with N from 0 to 20. N sets the fixed decimals of the tick labels.
195
+ - `dtick`, `width` and `height` must be above zero.
196
+ - One axis has 1000 ticks at most. A `dtick` that gives more ticks rejects with `E_INPUT` and the key path, for example `layout.xaxis.dtick`.
197
+
198
+ A tick label that needs more than 20 decimals uses exponent form, for example `1e-300`. A log axis that spans too many decades for minor ticks shows major ticks only.
199
+
200
+ ### Control helpers
201
+
202
+ The `./plot` subpath has six helpers. Each helper takes a transfer function `{ num, den }` with the coefficients of the numerator and the denominator, highest power first. Each helper returns `{ figure, numbers }`. The `figure` is a plot figure. Pass it to `render` as JSON text to draw it. The `numbers` hold the key values that the analysis derives from the same model.
203
+
204
+ Frequencies are in rad/s. Margins and phases are in degrees. Overshoot is in percent.
205
+
206
+ `bode` returns the crossover frequencies and the margins:
207
+
208
+ ```js
209
+ import { bode } from "@danielsimonjr/sciengjs/plot";
210
+
211
+ const { figure, numbers } = bode({ num: [10], den: [1, 1, 0] });
212
+ // numbers.gainCrossover is about 3.08, numbers.phaseMargin is about 17.96
213
+ ```
214
+
215
+ `step` returns the final value, the rise time, the settling time, the overshoot and the peak time:
216
+
217
+ ```js
218
+ import { step } from "@danielsimonjr/sciengjs/plot";
219
+
220
+ const { figure, numbers } = step({ num: [1], den: [1, 0.6, 1] });
221
+ // numbers.overshoot is about 37.2, numbers.riseTime is about 1.32
222
+ ```
223
+
224
+ The step helper measures the peak and the overshoot in the direction of the final value, so a negative DC gain gives a correct overshoot. A response with no peak before the end of the time grid has a `peakTime` of `null` and an `overshoot` of 0. A system with a pole on or right of the imaginary axis has no final value: each number is `null`. The rise and settling times are interpolated between samples.
225
+
226
+ The helpers reject with `E_INPUT` when the roots of the model do not converge, or when a computed value is not finite. A pole or a zero at 0 is exact and does not set the frequency range.
227
+
228
+ `impulse` returns the peak value and its time:
229
+
230
+ ```js
231
+ import { impulse } from "@danielsimonjr/sciengjs/plot";
232
+
233
+ const { figure, numbers } = impulse({ num: [1], den: [1, 1] });
234
+ // numbers.peak is 1, numbers.peakTime is 0
235
+ ```
236
+
237
+ `nyquist` returns the least distance from the curve to the point -1:
238
+
239
+ ```js
240
+ import { nyquist } from "@danielsimonjr/sciengjs/plot";
241
+
242
+ const { figure, numbers } = nyquist({ num: [10], den: [1, 1, 0] });
243
+ // numbers.minDistanceToMinusOne is about 0.299
244
+ ```
245
+
246
+ `poleZero` returns the poles and the zeros:
247
+
248
+ ```js
249
+ import { poleZero } from "@danielsimonjr/sciengjs/plot";
250
+
251
+ const { figure, numbers } = poleZero({ num: [1, 3], den: [1, 2, 5] });
252
+ // the poles are -1 + 2j and -1 - 2j, the zero is -3
253
+ ```
254
+
255
+ `rootLocus` returns the breakaway points and the gain at each point:
256
+
257
+ ```js
258
+ import { rootLocus } from "@danielsimonjr/sciengjs/plot";
259
+
260
+ const { figure, numbers } = rootLocus({ num: [1], den: [1, 3, 2] });
261
+ // numbers.breakawayPoints[0] is { re: -1.5, k: 0.25 }
262
+ ```
263
+
264
+ To draw a helper figure, pass it to `render`:
265
+
266
+ ```js
267
+ import { render } from "@danielsimonjr/sciengjs";
268
+ import { bode } from "@danielsimonjr/sciengjs/plot";
269
+
270
+ const { figure } = bode({ num: [10], den: [1, 1, 0] });
271
+ const { bytes } = await render({ type: "plot", source: JSON.stringify(figure), format: "html" }, {});
272
+ ```
273
+
173
274
  ## Roadmap
174
275
 
175
276
  The [build plan](docs/superpowers/plans/2026-11-01-scieng-engine-build.md) gives the order of the increments. Each increment adds one input type, its tests and one release. The [design](docs/superpowers/specs/2026-11-01-scieng-engine-design.md) gives the API, the stages, the security controls and the licence of each engine.
@@ -180,7 +281,7 @@ The [build plan](docs/superpowers/plans/2026-11-01-scieng-engine-build.md) gives
180
281
  | Mermaid | graph, mermaid, browser | class, state and sequence: released in 0.2.0; other types: released in 0.3.0 |
181
282
  | LaTeX math | math (MathJax) | svg, png and pdf are built; pdf needs the browser stage |
182
283
  | TikZ, circuitikz, LaTeX | tex (an own WASM build of `tex.web`) | trial builds pass |
183
- | Plots | plot | planned |
284
+ | Plots | plot | svg, png and html are built, not yet released; pdf is not planned |
184
285
  | SysML v2 | sysml | planned |
185
286
  | STEP, IGES, BREP | mesh (OpenCascade WASM) | trial build passes |
186
287
  | HTML to PNG, PDF and Markdown | html, browser | PNG and PDF: released in 0.3.0; Markdown: planned |
@@ -21,6 +21,7 @@ Version: each row gives the version that the engine pins.
21
21
  | raster | `@resvg/resvg-wasm` 2.6.2 | MPL-2.0 | a dependency |
22
22
  | raster | Liberation Serif Regular 2.1.5 (font, in `assets/fonts/`) | SIL Open Font License 1.1 | bundled |
23
23
  | html | `parse5` | MIT | not yet a dependency |
24
+ | plot (html output) | `plotly.js-dist-min` 4.2.0 (the pinned Plotly build, loaded by the html page from a CDN with a subresource integrity hash) | MIT | a devDependency. The package does not ship the build. The html page loads it from the CDN |
24
25
  | browser | Chromium or Google Chrome | Installed by the user. The engine does not ship it | not a package dependency |
25
26
 
26
27
  ## Notices
@@ -56,6 +57,10 @@ The Eclipse Public License 2.0 applies to Graphviz. The MIT licence applies to t
56
57
 
57
58
  The Apache License 2.0 applies to the MathJax packages. The BlueOak Model License 1.0.0 applies to the glob packages. The MIT licence applies to the other packages. Each package keeps its own licence text in its installed folder. The `dist/` folder holds the compiled code of this package only. It holds no code of the packages in the table: npm installs them as dependencies.
58
59
 
60
+ ### Plotly (MIT)
61
+
62
+ The MIT licence applies to `plotly.js-dist-min` 4.2.0. The package is a devDependency. The engine does not bundle the build into the package and does not copy it into the assets folder. The html output of the `plot` stage is a page that loads the pinned build from the CDN URL in the engine, with a subresource integrity hash. The page makes that request when a reader opens it. The licence text is in the installed package at `node_modules/plotly.js-dist-min/LICENSE`.
63
+
59
64
  ### Mermaid (MIT)
60
65
 
61
66
  The MIT licence applies to `mermaid` 11.17.2. The build writes the file mermaid.min.js into the assets folder. That file is generated at build time and is not committed. The licence text is in `assets/licenses/MIT-mermaid.txt`.
@@ -1,5 +1,12 @@
1
1
  /** Writes a value as a JavaScript literal that cannot end a script element or break a line. */
2
2
  export declare function scriptJson(text: string): string;
3
+ /** Writes a JSON value as a JavaScript object literal that cannot end a script element. */
4
+ export declare function scriptData(value: unknown): string;
5
+ /**
6
+ * A page that loads the pinned Plotly build with its Subresource Integrity value and draws one figure.
7
+ * The figure object comes from the engine after the reader accepts it. The page title is the title argument.
8
+ */
9
+ export declare function plotlyPageHtml(figure: unknown, cdnUrl: string, sri: string, title?: string): string;
3
10
  /** A page that shows one SVG document. The SVG text comes from the engine, not from the caller. */
4
11
  export declare function svgPageHtml(svg: string): string;
5
12
  /**
@@ -1,7 +1,33 @@
1
1
  const PAGE_STYLE = "html,body{margin:0;background:#fff}";
2
2
  /** Writes a value as a JavaScript literal that cannot end a script element or break a line. */
3
3
  export function scriptJson(text) {
4
- return JSON.stringify(text).replace(/[<>&\u2028\u2029]/g, (ch) => "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0"));
4
+ return escapeScriptText(JSON.stringify(text));
5
+ }
6
+ /** Writes a JSON value as a JavaScript object literal that cannot end a script element. */
7
+ export function scriptData(value) {
8
+ return escapeScriptText(JSON.stringify(value));
9
+ }
10
+ /** Escapes the characters that could end a script element or break a line in JavaScript source. */
11
+ function escapeScriptText(text) {
12
+ return text.replace(/[<>&\u2028\u2029]/g, (ch) => "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0"));
13
+ }
14
+ /** Escapes text for an HTML element body. */
15
+ function escapeHtmlText(text) {
16
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
17
+ }
18
+ /**
19
+ * A page that loads the pinned Plotly build with its Subresource Integrity value and draws one figure.
20
+ * The figure object comes from the engine after the reader accepts it. The page title is the title argument.
21
+ */
22
+ export function plotlyPageHtml(figure, cdnUrl, sri, title = "plot") {
23
+ return ('<!DOCTYPE html><html><head><meta charset="utf-8">' +
24
+ '<meta name="viewport" content="width=device-width, initial-scale=1">' +
25
+ `<title>${escapeHtmlText(title)}</title><style>${PAGE_STYLE}</style></head><body>` +
26
+ '<div id="sciengjs-diagram"></div>' +
27
+ `<script src="${cdnUrl}" integrity="${sri}" crossorigin="anonymous"></script>` +
28
+ `<script>(function () { const fig = ${scriptData(figure)}; ` +
29
+ 'Plotly.newPlot(document.getElementById("sciengjs-diagram"), fig.data, fig.layout, { responsive: false }); })();</script>' +
30
+ "</body></html>");
5
31
  }
6
32
  /** A page that shows one SVG document. The SVG text comes from the engine, not from the caller. */
7
33
  export function svgPageHtml(svg) {
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/browser/page.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,qCAAqC,CAAC;AAEzD,+FAA+F;AAC/F,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CACjC,oBAAoB,EACpB,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAC/D,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,2DAA2D,UAAU,mDAAmD,GAAG,sBAAsB,CAAC;AAC3J,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,MAAc;IAC5D,MAAM,MAAM,GACV,yJAAyJ;QACzJ,6EAA6E;QAC7E,2DAA2D,UAAU,CAAC,MAAM,CAAC,KAAK;QAClF,kFAAkF;QAClF,sHAAsH;QACtH,2FAA2F;QAC3F,6CAA6C;QAC7C,kHAAkH;QAClH,0CAA0C;QAC1C,oEAAoE;QACpE,gDAAgD;QAChD,sGAAsG,CAAC;IACzG,OAAO,CACL,2DAA2D,UAAU,uBAAuB;QAC5F,mCAAmC;QACnC,gBAAgB,MAAM,aAAa;QACnC,0BAA0B,MAAM,+BAA+B,CAChE,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../src/browser/page.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,GAAG,qCAAqC,CAAC;AAEzD,+FAA+F;AAC/F,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AAChD,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,UAAU,CAAC,KAAc;IACvC,OAAO,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,mGAAmG;AACnG,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;AAC5G,CAAC;AAED,6CAA6C;AAC7C,SAAS,cAAc,CAAC,IAAY;IAClC,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,MAAc,EAAE,GAAW,EAAE,KAAK,GAAG,MAAM;IACzF,OAAO,CACL,mDAAmD;QACnD,sEAAsE;QACtE,UAAU,cAAc,CAAC,KAAK,CAAC,kBAAkB,UAAU,uBAAuB;QAClF,mCAAmC;QACnC,gBAAgB,MAAM,gBAAgB,GAAG,qCAAqC;QAC9E,sCAAsC,UAAU,CAAC,MAAM,CAAC,IAAI;QAC5D,0HAA0H;QAC1H,gBAAgB,CACjB,CAAC;AACJ,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,2DAA2D,UAAU,mDAAmD,GAAG,sBAAsB,CAAC;AAC3J,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,MAAc;IAC5D,MAAM,MAAM,GACV,yJAAyJ;QACzJ,6EAA6E;QAC7E,2DAA2D,UAAU,CAAC,MAAM,CAAC,KAAK;QAClF,kFAAkF;QAClF,sHAAsH;QACtH,2FAA2F;QAC3F,6CAA6C;QAC7C,kHAAkH;QAClH,0CAA0C;QAC1C,oEAAoE;QACpE,gDAAgD;QAChD,sGAAsG,CAAC;IACzG,OAAO,CACL,2DAA2D,UAAU,uBAAuB;QAC5F,mCAAmC;QACnC,gBAAgB,MAAM,aAAa;QACnC,0BAA0B,MAAM,+BAA+B,CAChE,CAAC;AACJ,CAAC"}
@@ -14,3 +14,11 @@ export interface PinnedAsset {
14
14
  }
15
15
  /** The pinned builds of the engine. Each entry names the URL of the CDN and the local file. */
16
16
  export declare const PINNED_ASSETS: readonly PinnedAsset[];
17
+ /** The pinned Plotly version. It equals the exact devDependency plotly.js-dist-min of the repository. */
18
+ export declare const PLOTLY_VERSION = "4.2.0";
19
+ /** The SHA-256 of the pinned Plotly build, in hex. */
20
+ export declare const PLOTLY_SHA256 = "fde93964a7511bfa54c5745c49b5d0e41715cbde0cc5ebeae3f9a02de0396cae";
21
+ /** The Subresource Integrity value of the pinned Plotly build. The browser refuses other bytes. */
22
+ export declare const PLOTLY_SRI = "sha256-/ek5ZKdRG/pUxXRcSbXQ5BcVy94Mxevq4/mgLeA5bK4=";
23
+ /** The address that a plot page names for the pinned Plotly build. */
24
+ export declare const PLOTLY_CDN_URL = "https://cdn.jsdelivr.net/npm/plotly.js-dist-min@4.2.0/plotly.min.js";
@@ -10,4 +10,12 @@ export const MERMAID_CDN_URL = `https://cdn.jsdelivr.net/npm/mermaid@${MERMAID_V
10
10
  export const PINNED_ASSETS = [
11
11
  { url: MERMAID_CDN_URL, file: MERMAID_FILE, mime: "application/javascript" },
12
12
  ];
13
+ /** The pinned Plotly version. It equals the exact devDependency plotly.js-dist-min of the repository. */
14
+ export const PLOTLY_VERSION = "4.2.0";
15
+ /** The SHA-256 of the pinned Plotly build, in hex. */
16
+ export const PLOTLY_SHA256 = "fde93964a7511bfa54c5745c49b5d0e41715cbde0cc5ebeae3f9a02de0396cae";
17
+ /** The Subresource Integrity value of the pinned Plotly build. The browser refuses other bytes. */
18
+ export const PLOTLY_SRI = "sha256-/ek5ZKdRG/pUxXRcSbXQ5BcVy94Mxevq4/mgLeA5bK4=";
19
+ /** The address that a plot page names for the pinned Plotly build. */
20
+ export const PLOTLY_CDN_URL = `https://cdn.jsdelivr.net/npm/plotly.js-dist-min@${PLOTLY_VERSION}/plotly.min.js`;
13
21
  //# sourceMappingURL=pinned.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pinned.js","sourceRoot":"","sources":["../../src/browser/pinned.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AACzC,iGAAiG;AACjG,MAAM,CAAC,MAAM,cAAc,GAAG,kEAAkE,CAAC;AACjG,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAC7C,gHAAgH;AAChH,MAAM,CAAC,MAAM,eAAe,GAAG,wCAAwC,eAAe,sBAAsB,CAAC;AAS7G,+FAA+F;AAC/F,MAAM,CAAC,MAAM,aAAa,GAA2B;IACnD,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,wBAAwB,EAAE;CAC7E,CAAC"}
1
+ {"version":3,"file":"pinned.js","sourceRoot":"","sources":["../../src/browser/pinned.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,CAAC;AACzC,iGAAiG;AACjG,MAAM,CAAC,MAAM,cAAc,GAAG,kEAAkE,CAAC;AACjG,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAC7C,gHAAgH;AAChH,MAAM,CAAC,MAAM,eAAe,GAAG,wCAAwC,eAAe,sBAAsB,CAAC;AAS7G,+FAA+F;AAC/F,MAAM,CAAC,MAAM,aAAa,GAA2B;IACnD,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,wBAAwB,EAAE;CAC7E,CAAC;AAEF,yGAAyG;AACzG,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AACtC,sDAAsD;AACtD,MAAM,CAAC,MAAM,aAAa,GAAG,kEAAkE,CAAC;AAChG,mGAAmG;AACnG,MAAM,CAAC,MAAM,UAAU,GAAG,qDAAqD,CAAC;AAChF,sEAAsE;AACtE,MAAM,CAAC,MAAM,cAAc,GAAG,mDAAmD,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { BackendOutput, WorkerJob } from "../worker/protocol.ts";
2
+ /** Renders plot to html: one standalone page that loads the pinned Plotly build and draws the figure. The page title is the figure title, or "plot". */
3
+ export declare function render(request: WorkerJob["request"]): Promise<BackendOutput>;
@@ -0,0 +1,13 @@
1
+ import { plotlyPageHtml } from "../browser/page.js";
2
+ import { PLOTLY_CDN_URL, PLOTLY_SRI } from "../browser/pinned.js";
3
+ import { readPlotOptions } from "./backend-svg.js";
4
+ import { readPlotFigure } from "./reader.js";
5
+ /** Renders plot to html: one standalone page that loads the pinned Plotly build and draws the figure. The page title is the figure title, or "plot". */
6
+ export async function render(request) {
7
+ const { lenient } = readPlotOptions(request.options, ["lenient"]);
8
+ const { figure, warnings } = readPlotFigure(String(request.source), { lenient });
9
+ const title = figure.layout.title?.text;
10
+ const html = plotlyPageHtml(figure, PLOTLY_CDN_URL, PLOTLY_SRI, typeof title === "string" && title !== "" ? title : "plot");
11
+ return { bytes: new TextEncoder().encode(html), warnings };
12
+ }
13
+ //# sourceMappingURL=backend-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-html.js","sourceRoot":"","sources":["../../src/plot/backend-html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,wJAAwJ;AACxJ,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAA6B;IACxD,MAAM,EAAE,OAAO,EAAE,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAClE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IACjF,MAAM,KAAK,GAAI,MAAM,CAAC,MAAM,CAAC,KAAwC,EAAE,IAAI,CAAC;IAC5E,MAAM,IAAI,GAAG,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC5H,OAAO,EAAE,KAAK,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC;AAC7D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { BackendOutput, WorkerJob } from "../worker/protocol.ts";
2
+ /** Renders plot to png: the SVG of the figure, then the PNG of that SVG at the option scale. */
3
+ export declare function render(request: WorkerJob["request"]): Promise<BackendOutput>;
@@ -0,0 +1,12 @@
1
+ import { readScale, svgDocumentToPng } from "../graph/backend-png.js";
2
+ import { plotToSvg } from "./plotter.js";
3
+ import { readPlotOptions } from "./backend-svg.js";
4
+ /** Renders plot to png: the SVG of the figure, then the PNG of that SVG at the option scale. */
5
+ export async function render(request) {
6
+ const { lenient } = readPlotOptions(request.options, ["lenient", "scale"]);
7
+ const scale = readScale(request.options);
8
+ const result = plotToSvg(String(request.source), { lenient });
9
+ const png = await svgDocumentToPng(result.svg, request, scale);
10
+ return { bytes: png.bytes, warnings: [...result.warnings, ...(png.warnings ?? [])] };
11
+ }
12
+ //# sourceMappingURL=backend-png.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-png.js","sourceRoot":"","sources":["../../src/plot/backend-png.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEtE,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAA6B;IACxD,MAAM,EAAE,OAAO,EAAE,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3E,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC/D,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;AACvF,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { BackendOutput, WorkerJob } from "../worker/protocol.ts";
2
+ /** Reads the plot options. The only key is lenient, a boolean. Any other key rejects with E_INPUT. */
3
+ export declare function readPlotOptions(options: Record<string, unknown>, allowed: readonly string[]): {
4
+ lenient: boolean;
5
+ };
6
+ /** Renders plot to svg. The options are lenient only. */
7
+ export declare function render(request: WorkerJob["request"]): Promise<BackendOutput>;
@@ -0,0 +1,21 @@
1
+ import { RenderError } from "../errors.js";
2
+ import { plotToSvg } from "./plotter.js";
3
+ /** Reads the plot options. The only key is lenient, a boolean. Any other key rejects with E_INPUT. */
4
+ export function readPlotOptions(options, allowed) {
5
+ for (const key of Object.keys(options)) {
6
+ if (!allowed.includes(key))
7
+ throw new RenderError("E_INPUT", "plot takes no option " + key);
8
+ }
9
+ const lenient = options.lenient;
10
+ if (lenient !== undefined && typeof lenient !== "boolean") {
11
+ throw new RenderError("E_INPUT", "option lenient must be true or false");
12
+ }
13
+ return { lenient: lenient === true };
14
+ }
15
+ /** Renders plot to svg. The options are lenient only. */
16
+ export async function render(request) {
17
+ const { lenient } = readPlotOptions(request.options, ["lenient"]);
18
+ const result = plotToSvg(String(request.source), { lenient });
19
+ return { bytes: new TextEncoder().encode(result.svg), warnings: result.warnings };
20
+ }
21
+ //# sourceMappingURL=backend-svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backend-svg.js","sourceRoot":"","sources":["../../src/plot/backend-svg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,OAAgC,EAAE,OAA0B;IAC1F,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,uBAAuB,GAAG,GAAG,CAAC,CAAC;IAC9F,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAChC,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1D,MAAM,IAAI,WAAW,CAAC,SAAS,EAAE,sCAAsC,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,EAAE,CAAC;AACvC,CAAC;AAED,yDAAyD;AACzD,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,OAA6B;IACxD,MAAM,EAAE,OAAO,EAAE,GAAG,eAAe,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9D,OAAO,EAAE,KAAK,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;AACpF,CAAC"}
@@ -0,0 +1,93 @@
1
+ /** A transfer function G(s) = num(s) / den(s). Coefficients are in descending powers of s. */
2
+ export interface TransferFunction {
3
+ num: number[];
4
+ den: number[];
5
+ }
6
+ /** A complex number. */
7
+ export interface Complex {
8
+ re: number;
9
+ im: number;
10
+ }
11
+ /** The Plotly subset that the plotter reads. */
12
+ export interface ControlFigure {
13
+ data: Array<Record<string, unknown>>;
14
+ layout: Record<string, unknown>;
15
+ }
16
+ /** A figure plus the numbers that the analysis derived from the same model. */
17
+ export interface ControlResult<N> {
18
+ figure: ControlFigure;
19
+ numbers: N;
20
+ }
21
+ /** Options for the frequency helpers: the grid from wMin to wMax, in rad/s, with points samples. */
22
+ export interface FrequencyOptions {
23
+ wMin?: number;
24
+ wMax?: number;
25
+ points?: number;
26
+ }
27
+ /** Options for the time helpers: the end time tEnd and the number of points. */
28
+ export interface TimeOptions {
29
+ tEnd?: number;
30
+ points?: number;
31
+ }
32
+ /** Options for rootLocus: the largest gain kMax and the number of points. */
33
+ export interface RootLocusOptions {
34
+ kMax?: number;
35
+ points?: number;
36
+ }
37
+ /** Key values of a Bode plot: crossover frequencies in rad/s, margins in degrees. A null value means none was found. */
38
+ export interface BodeNumbers {
39
+ gainCrossover: number | null;
40
+ phaseCrossover: number | null;
41
+ gainMargin: number | null;
42
+ phaseMargin: number | null;
43
+ }
44
+ /**
45
+ * Key values of a step response: final value, rise and settling times, overshoot in percent, and peak time.
46
+ * The peak and the overshoot are measured in the direction of the final value. A system with a pole on or
47
+ * right of the imaginary axis has no final value, so every number is null. A response with no peak before
48
+ * the end of the time grid has a null peak time and an overshoot of 0.
49
+ */
50
+ export interface StepNumbers {
51
+ finalValue: number | null;
52
+ riseTime: number | null;
53
+ settlingTime: number | null;
54
+ overshoot: number | null;
55
+ peakTime: number | null;
56
+ }
57
+ /** Key values of an impulse response: the peak value and the time of the peak. */
58
+ export interface ImpulseNumbers {
59
+ peak: number;
60
+ peakTime: number;
61
+ }
62
+ /** The poles and the zeros of a transfer function, as complex numbers. */
63
+ export interface PoleZeroNumbers {
64
+ poles: Complex[];
65
+ zeros: Complex[];
66
+ }
67
+ /** The least distance from the Nyquist curve to the point -1. */
68
+ export interface NyquistNumbers {
69
+ minDistanceToMinusOne: number;
70
+ }
71
+ /** The breakaway points of the root locus, each with its gain k. */
72
+ export interface RootLocusNumbers {
73
+ breakawayPoints: Array<{
74
+ re: number;
75
+ k: number;
76
+ }>;
77
+ }
78
+ /** Validates a transfer function. Rejects with E_INPUT: empty, non-finite, leading-zero den, or improper. */
79
+ export declare function checkTransferFunction(input: unknown): TransferFunction;
80
+ /** The roots of a polynomial with descending coefficients. Each trailing zero coefficient gives an exact root at 0; Durand-Kerner finds the others. No convergence rejects with E_INPUT. */
81
+ export declare function polynomialRoots(coeffs: number[]): Complex[];
82
+ /** Bode plot. Crossovers and margins are null where no crossing exists. */
83
+ export declare function bode(input: unknown, options?: FrequencyOptions): ControlResult<BodeNumbers>;
84
+ /** Step response by exact zero-order-hold simulation. finalValue is the DC gain, null for a system that is not stable. */
85
+ export declare function step(input: unknown, options?: TimeOptions): ControlResult<StepNumbers>;
86
+ /** Impulse response of the strictly proper part, C e^(At) B for t > 0. A direct feedthrough term is not plotted. */
87
+ export declare function impulse(input: unknown, options?: TimeOptions): ControlResult<ImpulseNumbers>;
88
+ /** Pole-zero map: poles as crosses, zeros as circles. */
89
+ export declare function poleZero(input: unknown): ControlResult<PoleZeroNumbers>;
90
+ /** Nyquist plot of G(j omega) for negative and positive frequencies, with the critical point -1. */
91
+ export declare function nyquist(input: unknown, options?: FrequencyOptions): ControlResult<NyquistNumbers>;
92
+ /** Root locus of den + K num = 0 for K from 0 to kMax, with breakaway points on the real axis. */
93
+ export declare function rootLocus(input: unknown, options?: RootLocusOptions): ControlResult<RootLocusNumbers>;