Saving and exporting
Save the graph as plain data you can store and load again, and export what is on screen as an image.
toObject() on the ref returns the graph as plain data: its entities, its edges and the viewport
it's seen from. Store that object wherever you keep documents, and pass it back to KookieFlow
to open the board again. toImage() returns a picture of what's on screen.
Save the graph as data
toObject() returns a FlowObject, which holds three things.
| Field | Type | Meaning |
|---|---|---|
entities | Entity[] | Every entity in the graph. |
edges | Edge[] | Every edge in the graph. |
viewport | Viewport | Where the board was scrolled to and how far it was zoomed. |
JSON.stringify writes the whole object, as long as everything your app put on its entities can
be written. A socket whose widget is { component } holds a React component, which JSON can't
write, so give that socket its component again when you load the save.
save-button.tsx'use client';import type { RefObject } from 'react';import type { KookieFlowInstance } from '@kushagradhawan/kookie-flow';export function SaveButton({ flowRef, boardId }: { flowRef: RefObject<KookieFlowInstance | null>; boardId: string }) { const save = async () => { const flow = flowRef.current; if (!flow) return; await fetch(`/api/boards/${boardId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(flow.toObject()), }); }; return <button type="button" onClick={save}>Save</button>;}Load a saved graph
Give the saved entities and edges to useGraph as its starting graph, and the saved viewport to
defaultViewport.
board.tsx'use client';import { KookieFlow, useGraph, type FlowObject } from '@kushagradhawan/kookie-flow';export function Board({ saved }: { saved: FlowObject }) { const { entities, edges, onEntitiesChange, onEdgesChange, onConnect } = useGraph({ initialEntities: saved.entities, initialEdges: saved.edges, }); return ( <KookieFlow entities={entities} edges={edges} defaultViewport={saved.viewport} onEntitiesChange={onEntitiesChange} onEdgesChange={onEdgesChange} onConnect={onConnect} /> );}useGraph reads its starting graph, and KookieFlow reads defaultViewport, only when they first
mount. To open a different save in the same place, give the board a key that changes with the
save, such as <Board key={boardId} saved={saved} />. React then mounts a new board, so the
selection and any computed values start fresh as well.
To swap the graph without remounting, pass the save to setEntities and setEdges from
useGraph, and call setViewport(saved.viewport) on the ref. If useGraph keeps
undo history, use a key instead: the history isn't cleared, so an undo would bring
back a version of the graph you replaced.
A save holds no computed values. If your graph evaluates, load it with a key, and call
evaluateAll() in an effect when the board mounts, or put stored results back with
setSocketValue. If some types are manual because they cost money, call restoreAll() instead,
so that opening the board fetches their results rather than running them. The
Evaluation chapter covers all three.
What a save leaves out
A save keeps what describes the graph and drops what describes the moment. These aren't in it:
- Selection and drags.
selectedis removed from entities and edges, anddraggingfrom entities. - Computed values and evaluation status. Output values and the status the canvas tracks aren't
saved. A
data.statusyour app set is part ofdata, so it is saved. - Muted entities. Muting is kept by the store, not on the entity.
- Stacking. Which node was brought in front of another by pressing it isn't saved.
What a save adds
A save can also hold things your app never wrote into its own entities:
- Widget edits in flight. A widget value a person set that your
onWidgetChangehasn't written back yet is added to that entity'sdata.values, so the save matches what's on screen. - Values filled in from
entityTypes. An entity'sinputs,outputs,width,height,previewanddata.labelare saved when its type supplied them. A reloaded entity then states those values itself, so a later change to that type's definition no longer reaches it. If the type table should stay in charge of a type's sockets, deleteinputsandoutputsfrom those entities before you store the save. - Edge validity. An edge that names both of its sockets carries
invalid, which records whether the two socket types are compatible.
Read the current graph
getEntities() and getEdges() return the arrays the canvas is drawing right now. Use them in
code that holds the ref but not your graph state.
| Method | Returns | Meaning |
|---|---|---|
getEntities() | Entity[] | Every entity, as the canvas holds it. |
getEdges() | Edge[] | Every edge, as the canvas holds it. |
These are the canvas's own arrays, not copies, so don't change them. They differ from what
toObject() returns in two ways: selected and dragging aren't removed, and widget edits in
flight aren't added. To save the graph, call toObject().
Export an image
toImage(options) returns a data URL of what's on screen, or null when the canvas isn't
mounted. Its options have the type CaptureOptions.
| Option | Type | Default | Meaning |
|---|---|---|---|
pixelRatio | number | 2 | Image pixels for each pixel of the canvas on screen. Kept between 0.1 and 8. |
background | string | transparent | A CSS colour to fill behind the graph. |
type | string | 'image/png' | The image format, as a MIME type. |
The image covers the current view at the canvas's size multiplied by pixelRatio. A browser can
refuse a very large drawing buffer, and then the image comes out smaller than you asked for. JPEG
has no transparency, so pass a background with type: 'image/jpeg', or transparent areas turn
black.
To capture the whole graph, fit the view first. The camera moves to the new view on the next
frame, so capture inside requestAnimationFrame.
export-png.tsimport type { KookieFlowInstance } from '@kushagradhawan/kookie-flow';export function exportPng(flow: KookieFlowInstance) { const view = { ...flow.getViewport() }; flow.fitView({ padding: 40 }); requestAnimationFrame(() => { const url = flow.toImage({ pixelRatio: 2, background: 'white' }); flow.setViewport(view); // put the person's view back if (!url) return; const link = document.createElement('a'); link.href = url; link.download = 'graph.png'; link.click(); });}The image includes WebGL nodes, notes, text, widgets and media. The minimap, toolbars, native edit
fields and anything you pass as children live outside the canvas, so they're left out.