Go to appApp
Documentation Chapters

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. fileName arrives with the extension applied; extension is 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 follow black. 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: false hides 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 $derived and they stay current.

Instance and session

<script>
  let plotter
</script>

<GazePlotter bind:this={plotter} {load} />
  • plotter.resetLayout(): re-runs load.
  • 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') }