Embedding Cartesium graphs #
Cartesium exposes two deliberately different iframe contracts:
/embed/v1#doc=...is the versioned, passive external viewer for flashcards, feeds, and ordinary third-party sites. It is a live canvas backed by Cartesium's Wasm and worker renderer, but has no calculator, account, storage, or editing UI./embed#...is the existing interactive documentation viewer. It supports the documentation activation flow, presentation commands, and the existing read-only expression panel. It is an internal Cartesium documentation surface, not the minimal third-party contract.
The external v1 route accepts exactly one anonymous doc source or one public
at source. ?interaction=viewport remains explicitly unimplemented; it must
not be inferred from a URL or silently enabled by a future host.
Route examples use <base-path> for the configured URL mount prefix from
CARTESIUM_BASE_PATH. It includes its leading slash when non-empty; omit it
when Cartesium is served at a domain root. The current shared-subdomain test
deployment uses /cartesium.
External v1 URL and minimal host #
Build a GraphSnapshotV1, encode it with Cartesium's bounded
encodeGraphSnapshot implementation, and place the resulting Base64URL payload
in the fragment. Fragments are not sent in HTTP requests, so anonymous graph
data stays out of ordinary server request logs. The URL is:
<base-path>/embed/v1#doc=<compressed GraphSnapshotV1>
<base-path>/embed/v1#at=<encodeURIComponent(at://did/collection/rkey)>
The payload format is UTF-8 JSON for a validated GraphSnapshotV1, compressed
with gzip, then encoded as URL-safe Base64 without = padding (+ becomes
- and / becomes _). This is the same format produced by Cartesium's
ordinary 2D share-link action.
The smallest supported host needs no SDK or host-side JavaScript:
<iframe
src="https://cartesium.org/embed/v1#doc=BASE64URL_PAYLOAD"
title="Graph of a sine wave and a circle"
loading="lazy"
sandbox="allow-scripts allow-same-origin"
tabindex="-1"
referrerpolicy="no-referrer"
style="display:block;width:100%;aspect-ratio:3/2;border:0;pointer-events:none"
></iframe>
pointer-events:none is required for the passive contract. Pointer and wheel
events do not bubble across an iframe boundary, so an iframe cannot guarantee
that a host's page will keep scrolling when the pointer is over it. The
passive viewer installs no graph input handlers and is not focusable, while the
host-side rule guarantees scroll and focus ownership remains with the card or
page. An interactive host may use /embed instead and omit tabindex and
pointer-events:none; it must then document that the graph owns gestures while
active.
The iframe title is the accessible name for the embedded browsing context.
The surrounding card or page remains responsible for meaningful graph alt text
for users or clients that cannot execute the viewer (for example, “Sine wave
crossing a unit circle”). Do not put private data or credentials in the
fragment. HTTPS is recommended for both host and viewer to avoid mixed-content
failures. If the host has a Content Security Policy, it needs at least:
frame-src https://cartesium.org
The iframe requests no browser permissions and has no allow attribute. The
document response permits framing with Content-Security-Policy: frame-ancestors * and does not emit X-Frame-Options.
The current v1 safety limits are 750,000 Base64URL payload characters (the fragment parser allows 64 additional selector characters), 500,000 decoded JSON bytes, 200 expressions, 20,000 characters per expression source, and 500 characters for the graph name. These are published operational limits, not promises that their exact numbers can never change within v1. Input over a limit is rejected deterministically before renderer initialization. Keep anonymous links comfortably below about 64 KiB when possible and use a future published-record/reference contract for larger graphs.
The v1 fragment must contain exactly one source selector. The accepted forms
are unpadded Base64URL in #doc=... and a percent-encoded canonical AT URI in
#at=.... Construct the latter as:
`${base}/embed/v1#at=${encodeURIComponent('at://did/collection/rkey')}`
The viewer uses URLSearchParams for the one percent-decoding pass, then
strictly validates at://did/collection/rkey and its bounded DID, collection,
and record-key components. Duplicate selectors, doc plus at, a missing
selector, malformed or oversized AT URIs, and unknown fragment parameters
produce a structured error lifecycle message. URL query parameters are
currently rejected too; in particular, ?interaction=viewport is reserved
future work and is not silently enabled.
For an #at source, the browser makes exactly one credential-free request to
the same-origin resolver:
<base-path>/api/embed/v1/at?uri=<encodeURIComponent(at://did/collection/rkey)>
The resolver accepts only the configured Cartesium publication collections.
V1 records contain a validated GraphSnapshotV1; V2 records are accepted only
when their validated PublishedDocumentV2 is 2D. 3D, unknown collections,
malformed records, and oversized snapshots are rejected. The response contains
only {version: 1, kind: "2d", snapshot}; it is not an arbitrary AT record or
blob proxy. The server performs public DID/PDS reads without credentials, so a
host needs neither PDS CORS access nor an ATProto login.
Configured publication records use the application-level tid key. Cartesium's
own publishing flow is create/delete-only, but the resolver accepts any valid
record key in an allowlisted collection because AT records can be changed or
deleted externally. Successful resolver responses use a short bounded public
cache (max-age=60), so an external change or deletion becomes visible within
at most that cache window (subject to an intermediary's normal behavior).
Missing, deleted, private/inaccessible, and
invalid references never receive a cacheable error response: inaccessible
records intentionally collapse to the same publication-not-found state to
avoid disclosing record existence. Upstream failures report
publication-unavailable; the viewer does not retry automatically. Generic
clients should use the publication's static preview when the viewer reports an
unavailable or unsupported state.
The viewer is deterministic for a given snapshot and viewport size. It does not
read or write localStorage, sessionStorage, cookies, drafts, OAuth state,
or account state. A doc source performs no graph-data lookup; an at source
performs the single same-origin public resolver request described above. It
does still load its own JavaScript, module workers, Wasm, and styles, and normal network metadata
(IP address, timing, and the host's referrer policy if changed) can exist.
The resolver request explicitly uses credentials: "omit"; server-side PDS
reads also omit credentials. The initial iframe navigation may still carry
ambient cookies scoped to cartesium.org, but the viewer never reads them or
varies rendering by them. This is session-independent behavior: v1 output does
not depend on a Cartesium login or browser storage.
The document's referrerpolicy="no-referrer" avoids sending the flashcard URL
as a referrer in the baseline example.
The browser must support CompressionStream, DecompressionStream, module
workers, Canvas 2D, ResizeObserver, and Wasm. A client unable to run the
viewer should use the graph's content-addressed/static PNG preview instead;
generic feed clients should show that image, title, description, alt text, and
canonical link rather than execute iframe HTML.
Existing interactive viewer #
The existing interactive viewer accepts the documentation formats:
<base-path>/embed#preset=featured-lemniscate
<base-path>/embed#doc=<compressed GraphSnapshotV1>
It remains available for Cartesium's own documentation and is not silently changed into the passive route. The normal calculator URLs are:
<base-path>/calc/2d#preset=featured-lemniscate
<base-path>/calc/2d#doc=<compressed GraphSnapshotV1>
An interactive documentation embed can therefore offer an explicit “Edit” link without converting the document.
GraphEmbed #
apps/web/src/lib/components/GraphEmbed.svelte is the docs-facing wrapper. It:
- leaves the iframe unmounted until the reader activates the example;
- coordinates all instances in the JavaScript realm so only one iframe is live;
- displays the exact expression sources of a built-in preset directly on the documentation page;
- remembers the last reported viewport and restores it after reactivation;
- expands the same iframe wrapper with fixed positioning, avoiding reparenting or a second render;
- pushes one browser-history entry so Back, Escape, and the visible return button can restore the containing document; and
- provides Reset, Expand, and Edit controls without placing authoring or account concerns inside the viewer.
The current resting state accepts an optional authored previewUrl. For
documented built-in presets, it automatically resolves a checked-in image from
static/graph-previews; document snapshots and unregistered presets fall back
to the lightweight graph placeholder.
src/lib/embed/docs-preview-presets.json is the explicit preview manifest.
Regenerate its images from the real renderer with:
cd apps/web
npm run previews:update
The generator starts a loopback Vite server and headless Chromium, waits for
each graph to settle, and atomically writes a 960 by 600 PNG. Set
CHROMIUM_PATH if Chromium is not on PATH. Passing preset IDs after the
script generates only that subset; CARTESIUM_PREVIEW_SETTLE_MS adjusts the
post-render quiet interval. The ordinary npm run build includes the docs and
consumes the checked-in assets without requiring Chromium on the deployment
host. Run previews:update only when documented presets or rendering changes
make those assets stale.
The manual lifecycle page at <base-path>/docs/embed-lab renders twenty
image-backed resting examples for regression and memory testing. They create no
iframe, worker, Wasm engine, or live canvas until activated.
Declarative documentation and math #
Documentation pages may use the .svx extension to combine Markdown prose,
Svelte components, and TeX. MDsveX is part of the ordinary SvelteKit
preprocessing configuration, so there is no separate documentation build.
For example:
A matrix-vector inequality $Ax \le b$ defines the feasible region
$$
\left\{x \in \mathbb{R}^2 : Ax \le b\right\}.
$$
<GraphEmbed
title="Linear-program feasible region"
preset="linear-algebra-lp-region"
/>
remark-math recognizes inline $...$ and display $$...$$ nodes. The local
remark-mathjax transform renders those nodes to MathJax 4 CommonHTML during
the Vite build and adds page-adaptive CSS to the page's existing
<svelte:head>. The shipped page therefore contains already-typeset HTML and
does not load or execute MathJax in the browser.
The New Computer Modern WOFF2 files are self-hosted below
<base-path>/mathjax/newcm. Normal dev, dev:frontend, build, and
build:frontend commands copy them from the pinned npm package into the
generated static tree. The copied directory is ignored rather than checked in.
The reader-facing /docs reference uses a responsive documentation shell.
Navigation metadata lives in src/lib/docs/navigation.ts and links directly to
the reference page's syntax sections. Narrow windows replace the persistent
sidebar with an explicit Contents drawer; Escape, route navigation, or the
backdrop closes it. Markdown headings receive deterministic fragment IDs and
focusable self-links at build time through
scripts/remark-heading-links.mjs. The internal <base-path>/docs/embed-lab
lifecycle harness deliberately bypasses this reader shell.
External v1 lifecycle protocol #
The public viewer emits only two output messages from
apps/web/src/lib/embed/protocol.ts; it accepts no postMessage commands.
The message schema has independent versions from the URL API and document
format:
{
"type": "cartesium.embed.ready",
"protocolVersion": 1,
"embedApiVersion": 1,
"interaction": "none",
"title": "Sine wave"
}
Errors use the same version and interaction fields plus a bounded code and
sanitized message, for example type: "cartesium.embed.error" with
code: "missing-source", code: "invalid-document", or one of the public
resolution states (publication-not-found, publication-invalid,
publication-unavailable, or unsupported-publication). The complete current
error-code set is defined in the shipped TypeScript protocol module. The
effective v1 interaction is always none; there is no capability negotiation
or inbound command surface yet.
The existing /embed documentation viewer continues to use the separate
legacy cartesium:embed:* protocol, including presentation commands and
viewport updates. That protocol is not part of the public /embed/v1 contract.
For any lifecycle message, a host must validate the expected Cartesium origin,
event.source === iframe.contentWindow, and the complete schema before
consuming it; checking event.origin alone is insufficient. Cartesium's own
documentation wrapper applies both checks to its internal route.
The viewer response permits framing. The ordinary publication landing page
continues to use frame-ancestors 'none'; it is an Open Graph redirect surface,
not an executable embed.
Gesture ownership #
An inactive card behaves like ordinary document content. Its activation button must be used before it can consume drag, wheel, or pinch gestures. Once active, dragging pans the graph and wheel/pinch zoom it. Expanding does not construct a new renderer, so camera state survives the transition in both directions. The expanded viewer adds a read-only list of the loaded expressions beside the graph, or beneath it in a narrow window, so the definition remains visible without exposing editing controls.
For a future spatial relationship document, the outer diagram camera should own
gestures until a graph node is explicitly entered. The same GraphEmbed
component can then own gestures inside that node, while Back/Escape restores the
saved outer camera.
Security and deployment notes #
The viewer response's frame-ancestors * controls who may embed Cartesium; it
does not make the graph data trusted. Snapshot fields are decoded through the
bounded versioned validator, expression evaluation remains inside Cartesium's
Wasm/worker pipeline, and the v1 route cannot receive document replacement,
viewport, account, or publishing commands. The host must treat lifecycle
messages as advisory and validate origin/source as above. The host must not let
record-authored data choose an iframe origin.
Same-origin hosting is convenient for local development, but a dedicated
embed.cartesium.org origin is preferable for deployment: it separates the
untrusted graph-rendering surface from authenticated application state and makes
the allow-scripts allow-same-origin sandbox less dependent on same-origin
application privileges. That origin split is a deployment decision; the
contract above currently uses cartesium.org.
Internal architecture note #
The v1 viewer deliberately does not import CalculatorShell.svelte or
GraphCanvas.svelte. Those components own authoring, local drafts, account and
publication state, camera/tracing input, and PNG/GIF export; importing them
would put calculator-only module graphs on the external viewer's path and could
reintroduce server/runtime failures. EmbedV1Viewer.svelte converts the
validated snapshot to an anonymous GraphDocument2d, uses the shared scene and
spatial-tile computation, and renders retained resources through
PassiveGraphCanvas.svelte and CanvasRenderer. The passive surface installs
no pointer, wheel, touch, focus, or keyboard listeners. Its only lifecycle
messages are the versioned ready and error values described above.
The response header hook applies frame-ancestors * to both executable viewer
routes. Publication landing pages remain metadata/redirect surfaces with their
existing restrictive framing policy.
Deferred work #
- a narrowly specified
interaction=viewportmode with explicit gesture, focus, scrolling, and ephemeral-camera semantics; - authored preview generation for compressed documentation snapshots;
- an embeddable 3D viewer and 3D-specific
postMessagecamera protocol; - intersection-based prewarming just before a card enters the viewport;
- pausing and resuming slider animation through the protocol;
- keyboard instructions and broader focus/accessibility validation; and
- the separate HTML/MathJax/SVG spatial multigraph document system.