# 08 · Import & Export Astrolabe lets a user back up or transfer their entire workspace as a single JSON file, and bring data back in by importing such a file. These two whole-workspace actions are triggered from header controls labelled **Import** and **Export**. Import always merges with existing data; it never replaces what is already stored. Separately, a single chart can be exported on its own — its spec or its rendered image — from the Live Preview pane (see _Per-chart export_ below). That is distinct from the workspace Export: it gets _one_ chart out, not a backup of the library. ## Export Export produces one downloadable JSON file containing every snippet (see _Snippet Library_), every dataset (see _Datasets_), every custom chart theme (see _Live Preview → Chart theme_), and every uploaded font face (see _Live Preview → Chart theme → fonts_), wrapped in an envelope carrying format metadata. - **Trigger**: the **Export** header control runs the export immediately (no intermediate dialog). - **Contents**: all snippets, datasets, custom chart themes, and uploaded fonts currently stored, plus envelope metadata. - **Empty workspace**: if there are no snippets, the user is informed ("No snippets to export") and no file is downloaded — even if datasets, themes, or fonts exist. - **Filename**: `astrolabe-project-YYYY-MM-DD.json`, where the date is today's date (export day). - **Feedback**: on success a toast reports the counts, e.g. "Exported 4 snippets, 2 datasets and 1 theme" (the dataset, theme, and font clauses are omitted when their counts are zero; singular/plural wording adapts to the counts). ### Export envelope shape The downloaded file is a single JSON object: an envelope with a format `version`, an export timestamp, an exporter tag, and the data arrays. ```json { "version": "1.0", "exportedAt": "2026-06-03T12:00:00.000Z", "exportedBy": "Astrolabe", "snippets": [ /* full snippet objects (see Data Model) */ ], "datasets": [ /* full dataset objects (see Data Model) */ ], "themes": [ /* full custom chart theme objects (see Data Model) */ ], "fonts": [ /* uploaded font records, bytes base64-encoded (see Data Model) */ ] } ``` - `version` — export format version (currently `"1.0"`). - `exportedAt` — ISO 8601 timestamp of the export. - `exportedBy` — fixed identifier `"Astrolabe"`. - `snippets` / `datasets` / `themes` — arrays of complete records as defined in _Data Model_, each including its record `version` field. (This is the per-record schema version, not the envelope `version` above.) `themes` is additive: exports always write it, and importers treat it as optional, so pre-theme envelopes remain valid `"1.0"` files. - `fonts` — the uploaded font faces, each a complete record with its bytes **base64-encoded** (JSON cannot carry binary). Without this a theme or snippet referencing an uploaded font would import on another machine with only the family name, falling back to a system font. Like `themes`, `fonts` is additive — exports always write it, importers treat it as optional, so older envelopes remain valid `"1.0"` files. ## Per-chart export A single chart can be exported on its own, separately from the whole-workspace Export above. The affordance is an **Export** control in the **Live Preview pane header** — placed there because exporting an image needs the chart that is currently rendered, and "export this chart" reads naturally beside the chart you are looking at. It is a disclosure that reveals four actions in two groups: **Spec** (always available when a snippet is open): - **Copy spec** — copies the currently-shown spec (draft or published, matching the editor's view) to the clipboard as JSON. Confirmed by a success toast, since a clipboard write is otherwise invisible. - **Download JSON** — downloads the currently-shown spec as a `.vl.json` file. - **Referenced data** (shown only when the spec references one or more saved datasets) — a choice between **Inline** (default) and **Keep refs**. _Inline_ replaces each `{ data: { name } }` reference with the dataset's actual values so the exported spec renders standalone (outside Astrolabe), leaving the authored sizing untouched; _Keep refs_ exports the reference as written (which only resolves inside Astrolabe). It applies to both spec actions. If inlining is requested but a referenced dataset is missing from the library, the export is declined with a clear error. **Image** (available only when a chart is currently rendered — the actions are disabled, with an explanatory line, while the preview is empty or showing an error): - **Download PNG** — a rasterized image of the chart as shown. - **Download SVG** — a vector image of the chart as shown. When the chart uses an **uploaded** font (not a built-in roster or system family), that face is embedded into the SVG as a base64 `@font-face` rule, so the file renders the right type off-app instead of falling back to a system font; this happens automatically, with no option to configure. - **Resolution** (PNG) — `1×` / `2×` / `3×`, default `1×`. These are multipliers **of the display's pixel density**, so `1×` already matches on-screen crispness on a high-DPI (Retina) display; higher values produce larger images for print or zoom. (SVG is resolution-independent and ignores this.) - **Background** — `Theme` (default) / `White` / `None`. The chart itself renders on a transparent background (so on screen it shows the pane colour); export therefore fills it: _Theme_ matches the active theme's background, _White_ is always white, _None_ keeps it transparent. Applies to both PNG and SVG. Every format exports _what is on screen_: the same spec the editor shows and the same chart the preview renders (current fit mode included). With a _Theme_ background the exported image reflects the active light/dark theme. - **Filename**: derived from the snippet's name, made filesystem-safe (whitespace and illegal characters normalized; letters of any script preserved), with the format as the extension — e.g. `sales-by-region.png`, `sales-by-region.vl.json`. A name with nothing usable falls back to `chart`. No date or `astrolabe-` prefix (unlike the workspace export) — the user is exporting one named chart and wants its name on the file. - **Feedback**: a success toast naming the saved file (or confirming the copy); a clear error if the clipboard is blocked or the chart is not ready to rasterize. - **Availability**: the Export control is disabled when no snippet is open. The image actions additionally require a live rendered chart. ## Import Import lets the user pick a JSON file from their device; its contents are normalized, merged into the current workspace, and saved. - **Trigger**: the **Import** header control opens a file picker restricted to JSON files. After a file is chosen (or the picker cancelled) the control is ready to be used again immediately. ### Accepted inputs The importer recognizes several shapes so that both Astrolabe exports and looser snippet files work: - **Astrolabe export envelope** — an object with a `version` and a `snippets` array; optional `datasets`, `themes`, and `fonts` arrays are imported too. - **Bare array of snippets** — a top-level JSON array is treated as a list of snippets (no datasets, themes, or fonts). - **Single snippet object** — any other object is treated as one snippet. - **Older / foreign snippet shapes** — snippets that do not match the current model are normalized onto it: - Alternative field names are mapped: `content` → spec, `draft` → draft spec, `createdAt` → creation timestamp. - Missing timestamps are generated at import time (creation and modification set to now, or derived from the source timestamp when present). - Missing identifiers, names, comments, tags, dataset references, metadata, and record `version` are filled with defaults (a missing `version` is treated as the earliest shape and migrated up on read — see _Data Model_). - Such normalized imports are tagged `"imported"` so the user can find them. A snippet is treated as already in current Astrolabe format when it carries an ISO-style creation timestamp; in that case its existing fields (id, name, timestamps, spec, draft spec, comment, tags, dataset references, metadata) are preserved as-is, with sensible fallbacks for any missing field. ### Merge behavior - Imported snippets are **appended** to the existing library; nothing is overwritten or removed. - **ID collisions** (an incoming snippet whose id already exists) are resolved by assigning the incoming snippet a fresh unique id; the original snippet keeps its id. - Datasets, custom themes, and uploaded fonts are imported **before** snippets so that snippet dataset references resolve and the whole import rolls back together if the snippet write fails. - Imported custom themes (and fonts) always receive fresh ids from their library; an envelope's ids never displace existing records. - An unusable font record (missing family or bytes, or bytes that aren't valid base64) is skipped; the rest of the import continues. ### Name conflicts When an imported dataset's or custom theme's name already exists in the library, it is auto-renamed to a unique name rather than overwriting the existing one (see _Datasets_). - A numeric suffix is appended to the original name; further suffixes are added until the name is unique. - The renamed records are reported to the user via a warning toast listing each `original -> new` rename. - A dataset rename is propagated into the imported snippets that reference it; theme renames need no propagation (nothing references a theme by name). - If a single dataset fails to import, it is skipped and the rest of the import continues. **Fonts conflict differently — skip, not rename.** A font is identified by its family, which is the key embedded directly in a config's font slots, so a same-named face already in the library satisfies any incoming reference. When an imported font's family already exists, the **incoming face is skipped** and the existing one is kept (the references resolve to it) — rather than renamed to a copy. This also means re-importing your own backup adds no duplicate "Font 2" copies. Skipped fonts are listed in the import's warning toast. ### Storage limit handling Snippet storage has an approximate 5 MB budget (see _Snippet Library_ storage monitor). - If the incoming snippets would push total snippet storage over the budget, the user is warned about the overage amount, but the app still attempts to save the import. - If the save ultimately fails because the storage quota is exceeded, the user is told to delete some snippets and try again, and no partial snippet import is committed. - The storage check applies to snippets; datasets are stored separately and saved during the dataset phase above. ### Feedback - **Success**: a toast reports how many snippets (and datasets, themes, and fonts, when any) were imported, e.g. "Imported 4 snippets, 2 datasets and 1 theme". - **Renames / skips**: when datasets or themes were renamed, or fonts were skipped as already-present, the success message is shown as a warning toast that also lists the renames and skips. - **Empty file**: if no snippets are found in the file, the user is informed ("No snippets found in file") and nothing is imported — even if the file carries datasets, themes, or fonts. - **Quota failure**: a clear error advising the user to delete snippets and retry. - **Invalid file**: a non-JSON or unparseable file produces a clear error ("Failed to import. Please check that the file is valid JSON."); an unreadable file produces a read error. In all error cases the existing workspace is left unchanged.