A process graph, read as a graph: typed nodes, laid out, validated, and written back intact.
Renders nothing, like the rest of GestaltBI. It works out the picture — what each node is for, where it sits, what is wrong with it — and the host draws it: as SVG for a read-only view, or on a node canvas for an editable one.
Its job is to make an existing processing.json openable. Nothing has to be migrated, nothing is lost on the way back out. The canvas it exists for is the Studio, which is built on exactly this API.
import { readGraph, writeGraph } from '@gestaltbi/editor';
const graph = readGraph(processingJson);
graph.nodes // placed at { depth, lane }, with a kind, a summary and an arity
graph.edges // { from, to, socket }
graph.problems // cycles, bad arity, requires that name nothing
// ...and back, without losing anything it did not model
const config = writeGraph(graph.nodes, processingJson);
Every node arrives with somewhere to be, so a host renders without solving layout — multiply by
whatever spacing suits the surface and draw.
Positions are derived, never stored
Depth is the longest path from the raw frame, so a node always sits to the right of everything it
reads. Lanes are ordered by the average position of each node's inputs, which keeps edges short
without a full crossing-minimisation pass.
The point is what it means for a config nobody wrote in an editor. A processing.json written by
hand, or generated by a build script — as the Kickstarter bundle is — opens as a readable picture
straight away. Nothing has to be migrated to gain coordinates.
Nodes say what they are for
The op key tells you which class runs. kind tells you what it does to the data, which is what
somebody reading a pipeline for the first time actually needs.
kind
ops
reads as
prepare
format, clear
Make the text into dates and numbers
narrow
globalfilter, localfilter
Which rows are in play
derive
enhance, diffcalc, recognize
New columns from existing ones
summarise
aggregate, pivot, cohort
Fewer rows, rolled up
combine
join, union
Two branches back into one
relate
correlate
How columns move together
place
geocode, geojsonify
Rows onto a map
check
assert
Claims the data must support
settings
—
Options for a view, not a stage
inputs carries the op's arity, so a canvas knows how many sockets to draw and a validator can
catch a graph wired more ways than the op reads. A host with its own ops passes its own arities:
readGraph(config, { arity: { myop: 2 } });
A third of a config is not a stage
Entries named conf_* declare <code>"op": "noop"</code>, which is not a registered op. They never
run; they exist so views can read their options — which measure a chart opens on, which columns a
table shows, which currency a story uses.
config
entries
real stages
settings carriers
bundled sample
51
33
18
Everpix
18
13
5
Kickstarter
29
18
11
They are left off the canvas by default, because a node that cannot run is noise among nodes that
do — and put back untouched when the graph is written. Ask for them with includeSettings to show
them somewhere they make sense, beside the view they configure.
Problems are reported, not thrown
A config with a cycle in it is exactly the config somebody needs to look at. Refusing to draw it
hands them a stack trace instead of a picture of their mistake, so readGraph always returns a
graph and lists what is wrong with it.
graph.problems
// [{ severity: 'error', code: 'cycle', node: 'a',
// message: '"a" is part of a cycle: a -> b -> a' }]
code
means
cycle
A node reads, eventually, from itself. Laid out anyway, with the loop broken
missing-require
A require names a process that is not in the config
too-many-inputs
More inputs wired than the op reads
unknown-op
An op this host has not declared — a warning, not an error
The Studio
The node canvas this package exists for. Open a config repo by its org/repo slug, or a
processing.json from disk, and the graph draws itself — then add stages from a palette grouped
by what they do to the data, wire them by dragging between sockets, and copy the result back out.
It is a host, not a fork: every decision about what the graph means comes from this package, which
is why the editor and the read-only view inside GestaltBI cannot disagree about a config. Sockets
come from each op's declared arity; problems are listed as you work; the document is the source and
the canvas redraws from it.
Opening a config that already exists
An editor that quietly drops what it did not understand is one nobody can safely open a real config
with. writeGraph carries through the top-level keys, every option exactly as read, the settings
carriers that were never on the canvas, and the original key order — so a diff shows what you
changed and nothing else.
Reference
Extracted from src/index.ts when this page was built, so it
cannot drift from what the package actually exports.
function layout( nodes: GraphNode[], edges: GraphEdge[], problems: Problem[], ):
Place the nodes, left to right, without being told where anything goes.
Positions are derived rather than stored, which is the point: a processing.json written by hand, or generated by a build script, opens as a readable picture straight away. Nothing has to be migrated, and two people editing the same config never fight over coordinates.
Depth is the longest path from the raw frame, so a node always sits to the right of everything it reads. Lanes are then ordered by the average position of each node's inputs, which is the standard cheap way to keep edges short and mostly untangled.
Turn a process config into a graph that can be drawn.
Nodes come back placed, so a host can render without solving layout, and problems come back listed rather than thrown: a config with a cycle in it is exactly the config somebody needs to look at, and refusing to draw it leaves them with a stack trace instead of a picture of their mistake.
function writeGraph(nodes: GraphNode[], original?: ProcessLike | null): ProcessLike
Write a graph back into a process config.
Everything the editor did not model is carried through untouched: the top-level type, version and name keys, any settings carriers that were filtered out of the canvas, and every option on every node exactly as it was read. An editor that silently drops the parts it did not understand is an editor nobody can safely open an existing config with.
Pass the config it was read from as original. Without it the settings carriers cannot be restored, because they were never on the canvas.
The process name, which is its key in processing.json.
op?: string
kind: NodeKind
summary: string
Plain-language summary. A host with translations should prefer its own.
requires: string[]
Processes this one reads, in the order it named them.
inputs: number
How many inputs the op reads. Sockets to draw.
options?: any
settings: boolean
True for an entry that carries settings rather than running.
Around a third of every shipped config: conf_* entries declaring an op that is not registered. They belong beside the view they configure, not on the canvas, and a host is expected to filter them out.
leaf: boolean
Nothing requires this, so it is something a view subscribes to.
depth: number
Layout: how far downstream, counted in stages from the raw frame.
lane: number
Layout: position within the depth, chosen to keep edges short.
What the built-in ops are, grouped by what they do to the data.
Kept here rather than in @gestaltbi/stream because it is presentation: the engine needs to know that aggregate is a class, not that it belongs beside pivot under a heading a person would recognise. A host registering its own ops passes its own entries alongside these.
What this host knows that the package cannot assume.
arity?: Record<string, number>
How many inputs each op reads, keyed by op name.
Defaults to the built-in ops. Pass your own when the host registers extras, so a custom two-input op is drawn with two sockets instead of being reported as over-wired.
settingsOps?: string[]
Ops that exist but carry settings rather than running. Default: noop.
includeSettings?: boolean
Include settings carriers as nodes. Off by default: they are not stages.
What a node is for, in the reader's terms rather than the engine's.
The op key says which class runs; this says what it does to the data, which is what somebody looking at a pipeline for the first time needs. Nodes of a kind can share a colour and an icon without a host knowing sixteen op names.