Embedding GazePlotter
GazePlotter is also a Svelte component library. The workspace (the plot
canvas with its rail of controls, the settings pane and the dialogs) ships
as one <GazePlotter> component for use in your own application: Tauri or
Electron shells, web apps, other eye-tracking software.
The component is only the workspace. It has no top bar: Import, Export and Metadata are actions you put behind your own buttons (see Your own controls).
The component is self-contained: it injects its own design tokens into the document head and needs no server.
Requirements
- Svelte 5 and a Vite-based build.
worker: { format: 'es' }in your Vite config (parsing runs in a module web worker).- Peer dependencies:
jszip,lucide-svelte.
License
GazePlotter is GPL-3.0. The GPL restricts distribution, not use:
- Pointing a webview, iframe, or browser at a hosted GazePlotter: no obligations on your application.
- Shipping the built app as a separate program (own webview, data exchanged via files, URLs, or messages): aggregation. Keep the license notices and link the source repository; your application keeps its own license.
- Distributing a modified GazePlotter: publish the modifications under GPL-3.0.
- Importing the library into your codebase, as shown on this page: the combined application must be GPL-3.0.
Summary, not legal advice; the license text governs.
Minimal usage
<script>
import { GazePlotter } from 'gazeplotter'
</script>
<GazePlotter load={async () => []} /> load prepares the initial files: an async function returning File[]. [] opens an empty workspace; returned files parse immediately. Bytes from
anywhere can be wrapped:
const load = async () => {
const bytes = await fetchRecordingFromMyApp()
return [new File([bytes], 'recording.csv')]
} A ready-made loader for URLs:
<script>
import { GazePlotter, fromUrl } from 'gazeplotter'
const load = fromUrl('https://example.com/recording.csv')
</script>
<GazePlotter {load} /> The options prop
Everything else is one optional options object (GazePlotterOptions).
Every field is optional; every default is the plain web behavior.
<GazePlotter
{load}
options={{
defaultLayout: [{ type: 'scarf', x: 0, y: 0 }],
saveFile: (content, fileName, extension) => mySave(content, fileName),
openFiles: async () => myFilePicker(),
colors: { brand: '#0055ff' },
}}
/> defaultLayout: GridItemSnapshot[]: layout used when loaded gaze data carries none (fresh parses, empty workspace). Without it, the layout is chosen by the data: spatial data opens with a Scanpath on top. Event-only data always opens with Event Comparison, and workspace files keep their saved layout. Omitted snapshot fields get per-plot defaults.saveFile: (content, fileName, extension) => void: delivers one export file. Default: browser download.fileNamearrives with the extension applied;extensionis separate for native save-dialog filters.openFiles: () => Promise<File[]>: what the upload button opens. Default: browser file picker.[]means cancelled. Drag-and-drop is unaffected.colors: GazePlotterColors: partial palette override; unset keys keep the builtin, borders and shadows followblack. Applies reactively, so themes switch without remounting.
The web defaults are exported for wrapping instead of replacing: triggerDownload, openFilesViaBrowser, INGEST_FILE_ACCEPT.
Size and gestures
The workspace is a fixed frame you navigate like a map: drag empty space to
pan, Ctrl/Cmd + scroll or pinch to zoom, two fingers on touch screens. It is
one screen tall by default; set the CSS variable --gp-workspace-height on
any ancestor to size it. Give it a length (100vh, calc(100vh - 60px), 640px): a percentage resolves against the component’s own root, which has
no height. Notifications sit 8px above the bottom of the window; when your
page has a bar along the bottom edge, set --gp-toaster-bottom (for example calc(24px + 8px)) to lift them above it.
gestures: what the plain mouse wheel does over the workspace.'cooperative'(default) leaves it to the page, as an embedded map does, and shows a short “Ctrl + scroll to zoom” hint; use it when the workspace sits inside a scrolling page.'canvas'makes the wheel pan the workspace; use it when GazePlotter fills the screen.controls:falsehides the built-in rail (add plot, undo/redo, zoom), for hosts that provide their own.
<div style="--gp-workspace-height: 100vh">
<GazePlotter {load} gestures="canvas" />
</div> Your own controls
getActions() on the bound instance returns everything a toolbar needs,
with reactive flags for whether each action may run right now:
<script>
import { GazePlotter } from 'gazeplotter'
let plotter = $state()
const actions = $derived(plotter?.getActions())
</script>
<nav>
<button disabled={!actions?.canImport} onclick={() => actions?.openImport()}>
Import
</button>
<button disabled={!actions?.canExport} onclick={() => actions?.openExport()}>
Export
</button>
<button onclick={() => actions?.openMetadata()}>Metadata</button>
</nav>
<GazePlotter bind:this={plotter} {load} /> openImport(),openExport(),openMetadata(): the file picker (same pipeline as drag-and-drop), the export dialog, the dataset details.undo(),redo(),zoomIn(),zoomOut(),resetZoom(),zoomToFit().canImport,canExport,canShowMetadata,canUndo,canRedo,zoom: read them in markup or$derivedand they stay current.
Instance and session
<script>
let plotter
</script>
<GazePlotter bind:this={plotter} {load} /> plotter.resetLayout(): re-runsload.plotter.getActions(): see Your own controls.plotter.getSession(): the session object. Useful entries:ingest.loadFiles(files)(same pipeline as drag-and-drop),ingest.openAndLoadFiles()(what the upload button calls),ingest.applyEmpty()(clear all data).
Desktop shells (Tauri, Electron)
Electron works with the defaults: Chromium handles anchor downloads and the file input opens a native dialog.
In Tauri, anchor downloads do nothing on macOS, so wire saveFile to the
dialog and fs plugins (sketch):
import { save } from '@tauri-apps/plugin-dialog'
import { writeFile, writeTextFile } from '@tauri-apps/plugin-fs'
const saveFile = async (content, fileName, extension) => {
const path = await save({
defaultPath: fileName,
filters: [{ name: extension.slice(1).toUpperCase(), extensions: [extension.slice(1)] }],
})
if (!path) return
if (typeof content === 'string') await writeTextFile(path, content)
else await writeFile(path, new Uint8Array(await content.arrayBuffer()))
} Tauri configuration:
- Window
dragDropEnabled: false, so HTML5 drag-and-drop reaches the webview. - CSP must allow
blob:URLs and workers.
The __APP_VERSION__ build define is optional; without it, the citation
line and workspace metadata report unknown. To set it:
define: { __APP_VERSION__: JSON.stringify('your-app 1.0') }