Embedding figures in a web page¶
An IronLAB figure can be embedded in a web page as a live figure: a reader pans, zooms and rotates it, hides plots from its legend, reads its data with the pointer, saves it and exports it to PDF, in the page, with no software installed. The figure is drawn in the browser by the same engine that draws the desktop viewer and renders the gallery, so what the reader sees is what the author saw, and the PDF the reader exports is the PDF the author would have exported. This guide is for the author of a publication, a report or a documentation site who wants to put figures in a page. It assumes the figures already exist; how to build them is described in getting started, and the desktop viewer, whose gestures the embedded figure shares, in using the viewer.
Embedding needs no build step and no JavaScript of your own. A page needs one script, one stylesheet and, for each figure, a figure file and one HTML element.
Obtaining the bundle¶
The bundle is a directory of static files assembled by scripts/build-web.sh in the IronLAB repository:
| File | Contents |
|---|---|
ironlab.js |
The <ironlab-figure> element and its figurebar, as an ES module. This is the only file a page names. |
ironlab_core.js |
The JavaScript half of the figure engine, generated by wasm-bindgen, which ironlab.js imports. |
ironlab_core_bg.wasm |
The figure engine itself, compiled to WebAssembly: the scene compiler, the text engine with its fonts and LaTeX typesetter, the wgpu pipelines and the PDF exporter. |
ironlab.css |
The light-DOM defaults of the element: its place in the page, the behaviour of its fallback content and the theming tokens with their default values. |
ironlab_core.d.ts, ironlab_core_bg.wasm.d.ts |
TypeScript declarations of the engine's JavaScript interface, for a page that scripts the handle. |
README.md |
A summary of this page. |
LICENSE |
The licence under which the bundle is distributed. |
There are four ways to obtain it.
- Every release of IronLAB on GitHub attaches the bundle as
ironlab-web-<version>.tar.gz, with a.sha256checksum beside it. The archive holds the files above with no enclosing directory, so unpacking it into the directory of your choice is the whole installation. This is the way for a publication: the version is pinned by the file you unpacked. - ironlab.org serves the bundle of the current release at
https://ironlab.org/embed/ironlab.js,https://ironlab.org/embed/ironlab_core.js,https://ironlab.org/embed/ironlab_core_bg.wasmandhttps://ironlab.org/embed/ironlab.css. These URLs exist for the site's own gallery pages, and anyone content to track the current version may load the script and the stylesheet from them. A publication should not: its figures must open in ten years exactly as they open today, and a file served from ironlab.org changes with every release. - Every run of the
webjob of the repository's continuous integration attaches the bundle it built to the run as an artefact namedironlab-web, which can be downloaded from the run's page on GitHub. - A checkout of the repository builds it with
scripts/build-web.sh, whose--helpnames the tools it needs.
Copy the four files that begin with ironlab into a directory of your site, and add the script and the stylesheet to the page's <head>:
The paths are relative to the page, so the files may equally live in a subdirectory (assets/ironlab.js) as long as the four stay together: ironlab.js loads ironlab_core.js and ironlab_core_bg.wasm from its own location. The module defines the element when it loads, and loading it twice, or from two pages of the same site, is harmless. Self-hosting pins the version, and the figure files, the bundle and the page then form one self-contained record. The bundle is not published to npm.
The four files a page loads total 7.7 MB, or 3.8 MB in the compressed form every web server sends; it holds the whole engine, from the scene compiler and the text engine with its fonts to the wgpu pipelines for both web backends and the PDF writer. It is fetched once per page and cached by the browser thereafter, and every figure on the page shares it. The architecture reference describes the levers that will reduce it.
The bundle is part of IronLAB and is distributed under the same licence as the crates, the GNU Affero General Public License, version 3 or later (AGPL-3.0-or-later); the licence section of the README.md shipped with it says what that means for a page that shows a figure, and how to ask about other terms.
Producing the figure files¶
The element reads the files that Figure::save writes, in either encoding described in saving and loading: the Protocol Buffers .fig file, which is compact and the one to prefer for a page, or the JSON .fig.json file.
For the fallback content a page also wants the PDF that export_pdf writes and a PNG of the figure, which export_png writes as the renderer draws it; see exporting PNG for the resolution. The gallery's export command shows the complete set: cargo run -p ironlab-gallery -- export <directory> writes, for every gallery figure, its PDF, its .fig file, its .fig.json file and its PNG image at the resolution given by --dpi, into one directory ready to be served.
A figure file is what the author saved, so anything the author wants a reader to be able to do with it, such as restore the view with Refit, is defined by that file. The reader's changes are never written back to it.
The element¶
A figure is placed in a page with one element, whose content is the fallback shown until the figure draws:
<ironlab-figure src="pressure.fig" name="pressure" alt="Pressure along the chord">
<a href="pressure.pdf"><img src="pressure.png" alt="Pressure along the chord"></a>
</ironlab-figure>
| Attribute | Meaning |
|---|---|
src |
The URL of the figure file, relative to the page or absolute. Required. |
format |
The encoding of the file, fig or json. Without it the encoding is taken from the last extension of src: .json gives json and anything else gives fig, so a file named .fig or .fig.json needs no format. |
name |
The name of the files a reader downloads with Save figure… and Export PDF…, without an extension: pressure gives pressure.fig and pressure.pdf. Without it the name is the file name in src without its .fig, .json or .fig.json extension. |
alt |
The accessible name of the figure's canvas, read out by a screen reader in place of the drawing. Give every figure one, as you would an image; without it the name is used. |
toolbar |
auto, the default, shows the figurebar above the figure from the moment its first frame is drawn; hidden leaves it out, and everything on it remains reachable through the keyboard shortcuts and the JavaScript API. |
height |
Fixes the height of the frame, as a number of CSS pixels or as any CSS length. Without it the frame takes the width the page gives the element and the figure's own aspect ratio, so the figure fills it exactly. With it the figure is fitted into the frame at its own aspect ratio, as the desktop viewer fits a figure into its window, and the bands beside or above it are in the figure's background colour, or in the backdrop colour of the theme when that background is transparent. |
theme |
auto, the default, follows the reader's system preference through prefers-color-scheme; light and dark fix the theme. The theme colours the chrome (the figurebar, the datatips, the rubber band, the status line and the problems list) and the backdrop shown behind a figure whose background is transparent; it never recolours the figure itself, whose colours are the author's. |
The element is a block that takes the width of its container, so it lays out like a block image and can be placed in a <figure> with a <figcaption>, in a grid, or in a column of text.
A figure begins loading only when its element comes within 200 pixels of the viewport, so nothing is fetched for a figure far down a long page until the reader scrolls towards it, and the figure is released when its element is removed from the document.
Fallback content¶
Whatever the element contains is shown until the figure's first frame is drawn and is then hidden behind the live figure. It is kept, with a status line beneath it saying why, when the browser offers neither WebGPU nor WebGL2, when the figure file cannot be fetched or read, or when the graphics device fails; and it is all a reader sees when the script is blocked or fails to load. The element therefore enhances a page that already works without it, and a reader without a graphics device, a reader who prints the page and a search engine all see the fallback.
The pattern above, a PNG of the figure linking to its PDF, is the one the gallery uses and the one to copy: the PNG is the figure as the engine drew it, written by export_png, and the PDF is the vector figure at full quality. The alt text of the image should say what the figure shows, as the element's alt does. The element works without any fallback too.
Reading a figure¶
The embedded figure behaves as the desktop viewer does, with the differences below, so using the viewer describes each gesture and what it changes. Every gesture sets a property of the figure, exactly as it does in the viewer, and every one can be undone.
The figurebar¶
Above the figure, unless toolbar="hidden", a bar holds the following controls.
- Pan, Zoom and Rotate choose what dragging does, as the viewer's tools do. Rotate appears only for a figure with a three-dimensional axes.
- Refit discards the limit and view changes of every axes, restoring the views the file was saved with, as the viewer's Refit does.
- Undo and Redo step through the reader's gestures, as in the viewer's undo and redo.
- A problems indicator appears at the right when the figure has something to report, exactly as the viewer's problems indicator does, and lists the problems when clicked.
- Save figure… downloads the figure as it is currently shown, with the reader's limits, views and plot visibility, in the encoding of the source file: a
.figfile for a.figsource and a.jsonfile for a JSON source, named fromname. - Export PDF… downloads a PDF of the figure as it is currently shown. The page has the same properties as one written by the API, described in exporting PDF, and is written with the default settings for dense surfaces and three-dimensional axes; an artist that must be drawn as an image, and the renders that verify a three-dimensional axes, are drawn on the reader's own graphics device. The warnings of the export report, such as an artist drawn as an image or an axes the exporter could not verify, are counted in a status line beneath the figure, listed in the browser's console and dispatched in the
ironlab-exportevent.
Gestures¶
Dragging pans, zooms or rotates the axes under the pointer according to the chosen tool; a touch drag does the same rather than scrolling the page, and pinching with two fingers, or on a trackpad, zooms. Double-clicking an axes restores its view. Clicking a legend entry hides or shows its plot. Resting the pointer near a point of a line or a scatter, or over an image, shows a datatip, with the point ringed or the pixel outlined as in the viewer.
Scrolling the mouse wheel over a figure zooms it only after the figure has been activated by a click or a touch. A page is scrolled with the wheel, and a figure that took the wheel as soon as the pointer crossed it would trap the reader in the middle of the page; a figure that never took it could not be zoomed. The figure therefore takes the wheel only while it is active, and it is deactivated by pressing Escape, by keyboard focus leaving it or by a click anywhere outside it, after which the wheel scrolls the page again. A hint over the figure says so while the wheel is not yet taken. Dragging and pinching need no activation.
Keyboard shortcuts¶
While the figure or one of its buttons has keyboard focus, R refits it, ⌘Z (Ctrl+Z away from macOS) undoes the most recent gesture and ⌘⇧Z (Ctrl+Shift+Z) redoes it. Escape closes the problems list and gives the wheel back to the page. The shortcuts are the viewer's, and they reach the page's own handlers when no figure has focus.
What the page does not offer¶
The page has no figure browser, because each element shows one figure and the page itself is the collection, and no property editor: a reader changes the view of a figure and the visibility of its plots, and the author's properties stay as the file holds them.
The JavaScript API¶
A page that wants more than the figurebar drives the element from a script. Each <ironlab-figure> element has the following members once ironlab.js has loaded.
| Member | Meaning |
|---|---|
ready |
A promise that resolves with the figure's handle when the figure has drawn its first frame, and rejects with the reason when it cannot. |
handle |
The figure's handle in the engine, null until the figure has been opened and after the element is disconnected. It is the FigureHandle of the wasm module, whose methods are declared in ironlab_core.d.ts: among them problems(), which lists what is wrong with the figure, and size_pt(), the size of its page in points. |
session |
A promise of the page's one Session, shared by every figure on the page; its backend() method answers webgpu or webgl2. |
refit() |
Refits the figure, as the Refit button does. |
save() |
Downloads the figure as Save figure… does. |
exportPdf() |
Exports and downloads a PDF as Export PDF… does, and returns a promise that resolves with the export report's warnings once the download has begun, or rejects, after the status line has reported the failure, when the export fails. |
The module itself exports session(), the promise of that one session, version(), which resolves with the version of the engine, and the element's class, IronlabFigure, as its default export.
The element dispatches four events. All four bubble and cross the shadow boundary, so a listener on document hears every figure on the page.
| Event | When |
|---|---|
ironlab-ready |
The first frame has been drawn and the fallback replaced. detail.handle is the handle. |
ironlab-error |
The figure could not be shown: the file could not be fetched or read, no graphics device could be obtained, or the device failed. The fallback stays, and detail.message says why. |
ironlab-change |
The reader changed the figure, by a gesture, the wheel, a click on a legend entry, a double click, Undo, Redo or Refit. It is dispatched exactly when the figure changed, never for an input that changed nothing, and carries no detail. |
ironlab-export |
A PDF was exported. detail.warnings lists the export report's warnings, each as { subject, detail, explanation }: what was drawn as an image rather than as vectors, and which three-dimensional axes could not be verified. |
const figure = document.querySelector("ironlab-figure");
figure.addEventListener("ironlab-export", (event) => console.log(event.detail.warnings));
const handle = await figure.ready;
console.log(handle.problems());
figure.refit();
Theming¶
The element colours its chrome, meaning the figurebar, the datatips, the rubber band, the hint, the status line and the problems list, through CSS custom properties named --ironlab-*, declared on the element with one set of values for the dark theme and one for the light. ironlab.css lists them with those values, at no specificity, so that any rule a page writes on ironlab-figure wins over them: a page restyles every figure by setting a property on ironlab-figure in its own stylesheet, and a single figure by setting it on that element. A property set this way overrides the theme attribute. The figure itself is never restyled: its colours, fonts and sizes are properties of the figure model and belong to its author.
| Property | What it colours |
|---|---|
--ironlab-bg |
The panels: the figurebar, the datatip, the hint, the status line and the problems list. |
--ironlab-text |
The text of the panels. |
--ironlab-weak |
Secondary text: the hint, the status line and the explanation beneath a problem. |
--ironlab-widget |
The face of a button. |
--ironlab-widget-hover |
The face of a button under the pointer. |
--ironlab-stroke |
The borders of buttons and panels, and the separator between the figurebar's tools. |
--ironlab-accent |
The face of the pressed tool button, and the wash of the rubber band. |
--ironlab-accent-text |
The text of the pressed tool button. |
--ironlab-selection |
The edge of the rubber band, the datatip's marker and the focus ring. |
--ironlab-problem |
The problems indicator. |
--ironlab-canvas-bg |
The backdrop behind a figure whose background is transparent. |
--ironlab-frame |
The line around the whole element, figurebar and figure together. Transparent unless a page sets it. |
--ironlab-font |
The typeface of the panels. |
--ironlab-mono |
The monospaced typeface of a datatip's values. |
The backdrop is the colour shown behind a figure whose background is transparent. The browser's surface cannot itself be transparent, so a transparent figure is drawn over the backdrop, which follows the theme so that such a figure sits on the page's own colour in either mode. The element reads --ironlab-canvas-bg again whenever the page's colour scheme changes, whether through the system preference or through an attribute or class the page toggles on <html> or <body>, so a site with a theme switch needs to do nothing more than map its own variables onto the properties above. The gallery pages of this site do exactly that, in the stylesheet the gallery generator writes.
Hosting¶
The bundle is static files and is served by any web server, including GitHub Pages, with four points to check.
ironlab_core_bg.wasmshould be served with the media typeapplication/wasm, which every common server does by extension. A server that serves it with another type makes the browser take a slower path to compiling the module, and one that serves it as text breaks it.- Figure files are binary and need no particular media type;
application/octet-streamorapplication/x-protobufis fine for a.figfile andapplication/jsonfor a.fig.jsonfile. A figure file on a different origin from the page must be served with a CORS header that admits the page's origin, as any file a page fetches must. - No cross-origin isolation headers are needed: the engine runs on the page's one thread and uses no shared memory and no worker.
- The page must be served over HTTP or HTTPS rather than opened from the file system, because ES modules and
fetchdo not work fromfile:URLs. Serve the directory locally while writing a page, for instance withpython3 -m http.server.
Limits¶
The engine needs WebGPU or WebGL2. Every current desktop and mobile browser has WebGL2; where neither is available, or the graphics driver is blocked, every figure on the page keeps its fallback content with the status line "This figure needs WebGPU or WebGL2 to be interactive." A module that fails to load or initialise is reported differently, with the status line "The figure engine could not be started" followed by the browser's reason, because that is a fault of the bundle or of its hosting rather than of the reader's browser.
Under WebGPU one graphics device serves every figure on a page, and a page may hold as many figures as it likes. Under WebGL2, which the element falls back to where WebGPU is not available, every figure has a device of its own, because the device is the canvas element's own context, and browsers allow about sixteen live WebGL contexts per page before they begin discarding the oldest. A page of many figures is nevertheless fine, because a figure is created only when it scrolls near the viewport and released when it is removed from the document: a long article of thirty figures creates the few that are on screen, and a page of a dozen figures in view at once is safe. Thumbnails that stay in view together, such as the grid of the gallery index, should be static images linking to pages that embed the figures.
Troubleshooting¶
- The fallback image stays, with a status line beneath it. The status line and the browser's console say why. The usual causes are a figure file that was not found (a wrong
src), a browser without WebGPU or WebGL2, and a graphics device that failed. - The status line says "The figure engine could not be started". The wasm module was fetched but could not be compiled or initialised, so no figure on the page can start in any browser. The reason that follows names the failure. A module that is served with the wrong media type, truncated by a proxy, or paired with an
ironlab_core.jsfrom a different build fails in this way. Serve the four files of one bundle together, unmodified. - The fallback image stays and there is no status line. The script did not run: it was blocked by a content security policy or an extension, or
ironlab.jscould not be loaded. The console names the file that failed. Check that the four files are together and thatironlab_core_bg.wasmis served asapplication/wasm. - Scrolling the wheel over a figure scrolls the page. The figure is not active; click it first.
- Figures near the foot of a long page do not appear until the reader reaches them. That is the lazy creation described under limits, and it is intended.
- The exported PDF carries a warning. The status line counts the export report's warnings, the console lists them and the
ironlab-exportevent carries them; they are described in exporting PDF. The most common is a three-dimensional axes that could not be verified, which is written back to front rather than as an image.