seamonster
| Install | |
|---|---|
npm install seamonster |
|
| Latest Version: | 0.4.2 |
| Vue: | 3 |
| License: | Apache-2.0 |
| Last Updated: | Oct 23, 2026 |
| Links: | GitHub · npm |
SEAMONSTER
Early release. This is 0.3: the API may change between minor versions until 1.0, so pin an exact version if you depend on it.
Vue 3 widgets for wrangling data in flowgraphs. The main one is the
flowgraph editor: <FlowgraphEditor /> is the whole thing, canvas, panels and
menus. The others are its menus and some general-purpose data widgets, each
its own entry:
| Import | What |
|---|---|
seamonster |
the flowgraph editor: FlowgraphEditor, its panels (EditorPanels, DataPanel...), defineTheme, and everything in /panels |
seamonster/panels |
the editor's docked panels over any content: PanelHost, FlowPanel (widgets/panels); no Vue Flow needed |
seamonster/menu |
declarative, context-sensitive context menus (widgets/menu) |
seamonster/controls |
control panels: selects and checkbox groups (widgets/controls) |
seamonster/doc |
markdown documents (widgets/doc) |
seamonster/scene |
a 3D scene, on TresJS (widgets/scene) |
seamonster/sheet |
spreadsheet tables (widgets/sheet) |
seamonster/spatial |
maps, with /leaflet and /deck renderers, and the editor's Map panel in /leaflet (widgets/spatial) |
seamonster/style.css |
every widget's CSS, once, before the app's own |
SEAMONSTER has no server, storage or commands of its own: the host supplies
them as props, and it works without any of them. DAVY JONES' LOCKER
(davy-jones-locker on npm and PyPI) is the framework that supplies them: a
browser shell and a Python engine.
import { FlowgraphEditor } from 'seamonster'
import 'seamonster/style.css'
Vue, reka-ui and Vue Flow (@vue-flow/core, @vue-flow/background) are peer
dependencies. The other widgets' libraries are optional peers, needed only
by the widget that uses them: three and TresJS (scene), Leaflet, deck.gl and
shpjs (spatial), marked (doc). A host linking SEAMONSTER from a folder
(file:) should list vue, reka-ui, @vue-flow/core,
@vue-flow/background and seamonster in Vite's resolve.dedupe: a second
copy of Vue breaks reactivity, and of SEAMONSTER splits its menu registry.
npm run build # dist/: an ES module per entry, and style.css
Structure
index.js the editor's entry (seamonster)
styles.css the editor's tokens (--flow-*) and rules
widgets/ what a host uses whole
FlowgraphEditor.vue the editor: a PanelHost with the canvas, title, panels and menus
panels/ PanelHost: panels, rails and their keys over any content (seamonster/panels)
EditorPanels.vue the five below, in order: the editor's panels unless the host gives its own
TerminalPanel.vue left panels, in this order (T): the selected node's log, or the app's
LibraryPanel.vue (L): node cards to drag onto the canvas
FlowgraphsPanel.vue (G): Execute Graph
DataPanel.vue bottom panel (D): the selected node's output (a tab per table)
ManagePanel.vue right panel (M): the selected node's properties
menu/ controls/ doc/ scene/ sheet/ spatial/ the other widgets, each with its README
(spatial/ has the Map panel, a panel for the editor's `panels` slot)
components/ what the editor is built from
FlowCanvas.vue the pannable layer: Vue Flow, with its nodes and wires
FlowNode.vue a node: its face, pins, kind tag and output hint
NodeFace.vue a node's face, also the Library's cards (scaled)
FlowWire.vue a wire: bezier curve between two pins
FlowConnectionLine.vue the wire being dragged from a pin
FlowPanel.vue the base panel every panel type wraps
FlowPanelRails.vue collapsed / side titles on each window edge
panelMenus.js the panels' menu definitions and commands
DataTable.vue one page of a table, and its pager
NodeProperties.vue Manage's fields: ID, Name, each kind's own, Run
AssistantField.vue Manage's AI assistant, above a node's SQL or code
NodeField.vue one of a kind's declared fields in Manage (text, number, select, code, file...)
DataIngestFields.vue, ExportInputField.vue, SubnetFields.vue the built-ins' custom fields (file chooser, Input picker, a subnet's Open, pins and graph)
CommandField.vue the Terminal's command field: completion, history
ServerStatus.vue bottom left: the server connected (green) or not (blue)
flowMenus.js the graph's context menus and their commands
lib/
flowGraph.js the graph: nodes, wires, edits, runs, stored files (one per window)
flowGraphKey.js its injection key (FLOW_GRAPH), for panels outside the editor's entry
results.js a run result's tables, one at a time or all of them
graphDocument.js the graph as a saved document, and back
graphStorage.js loads the graph from the host's storage, saves edits to it
graphRunner.js runs nodes (and their upstream) on the host's SQL engine, subnets level by level
subnets.js subnets: their graphs, pins (from Input and Output nodes) and paths
jsSandbox.js runs a JavaScript node's code in a sandboxed iframe's worker
fileFormats.js the file formats DataIngest reads and DataExport writes
xlsx.js a workbook's sheet names, and repairing DuckDB-wasm's xlsx output
zip.js reading and writing zips (shapefiles, in and out)
geoExport.js DataExport's GeoJSON, KML, GeoPackage and shapefiles
crs.js what GDAL and PROJ say about a CRS (its WKT, datum, a dropped datum shift)
spatialChecks.js warnings about spatial SQL: measuring in degrees, a dropped datum shift
nodeKinds.js import node kinds from here: the registry, with the built-ins defined in it
kindRegistry.js the registry: defineNodeKind, defineNodeCategory, node IDs, reference names
builtinKinds.js SEAMONSTER's own kinds (SQLQuery, JavaScript, DataIngest...), defined like an app's
sqlText.js DuckDB SQL pieces the runner and the built-ins share
panelLayout.js all panel layout state and rules (one per window)
themes.js the theme registry: the built-in themes and defineTheme
keyboard.js isTyping: where single-key shortcuts must not fire
commands.js the Terminal's commands: registry, parser, completion, running one
The canvas fills the window and can be larger than it. Panels and the title are positioned against the window, not the canvas, so they stay put while it pans.
Props
| Prop | Default | |
|---|---|---|
title |
'SEAMONSTER' |
the name top left (and the window's accessible name) |
theme |
'FLOW' |
any case: a built-in theme, FLOW (rounded, category gradients), FLOWDARK (rounded, light to dark grey), LUX (sharp, brushed metal in the category's colours) or LUXDARK (sharp, obsidian; the node under the pointer shows its category colour down the left edge), or one the app defines (see Themes) |
storage |
none | keeping the open graph and ingested data (see Keeping the open graph) |
sql |
none | the engine nodes run on (see Running nodes) |
server |
none | the server engine (see Running nodes) |
autoHideRails |
true |
open panels leave the rails, as long as nothing on their edge can cover them; false keeps a rail for every panel (see Panels) |
terminalCommands |
none | the Terminal's commands, a registry from createCommandRegistry (see Terminal commands). Without it the Terminal has no command field |
Themes
A theme is a name, the theme it's based on, and token values over that theme's, set on the window (themes.js). The four built-ins are node looks (see Styling); an app defines its own before FlowgraphEditor mounts, usually to change the brand gradient (the title, and hovered panel titles, rails and menu items):
import { defineTheme } from 'seamonster'
defineTheme({
name: 'EMBER',
base: 'LUX',
tokens: { '--flow-brand-from': '#f7b955', '--flow-brand-to': '#d1741f' },
})
<FlowgraphEditor theme="EMBER" /> then uses it. A theme can be based on
another app theme; names are matched in any case, and a built-in's name can't
be reused. themeNames() lists them all (the Terminal's f.Theme will).
Canvas
The canvas is Vue Flow (@vue-flow/core). Only its
structural CSS is imported (in FlowCanvas.vue), not its default theme; nodes
and wires are SEAMONSTER's own components, rendered through the #node-pipeline and
#edge-wire slots and styled by .flow-node*, .flow-handle* and
.flow-wire* in styles.css. Zoom runs from 0.25 to 2.
The graph lives in flowGraph.js, which
FlowgraphEditor creates and provides like the panel layout. It owns the window's
Vue Flow store (so <VueFlow> in FlowCanvas picks it up) and every edit:
- Adding nodes: from the canvas menu at the right-clicked point; by
dropping a wire dragged from a pin on empty canvas, which opens the same
node menu and wires the new node in, with its pin at the drop point; or by
dragging a card from the Library, which lands where the card was held
(HTML drag and drop, type
application/x-flow-node-kind). - Wires: drag from a pin to a pin. Output to input only, and never back into a node's own upstream: the graph stays acyclic.
- Selecting: click a node (outlined) or a wire (dashed). One thing is selected at a time (no box or multi-select); clicking the canvas deselects. The Manage panel shows the selected node.
- Deleting: Delete/Backspace deletes what's selected. A node takes its wires with it; a wire alone leaves both nodes, and the input pin it fed empties unless another wire feeds it. Nodes and wires also have a Delete context menu. Ctrl/Cmd+Z undoes deletions, most recent first; no other edit can be undone.
- IDs are
<prefix><n>per kind, the lowest freen(sqlnode1), and unique, counting deleted nodes that undo could restore. A node's output is read downstream as<id>_data, or<id>_<output name>once it's named (shown when hovering its output pin). - Naming (Manage): Name labels the node, and a renamed node shows its kind
above it. While the ID is still the generated one, naming also sets the ID
to the name as an identifier (
Sales 2024→sales_2024, thensales_20241if that's taken); after that the ID is left alone. The ID can be edited directly (not empty, not in use), and its wires follow it; other nodes' code that names the old ID doesn't. Fields commit on blur or Enter.
Nodes and wires aren't focusable, so keyboard focus stays on the window when the node under it is deleted.
Keeping the open graph
FlowgraphEditor takes a storage prop from the host app, { load(), save(doc) }
(plus the file methods under Running nodes), and never stores anything itself
(graphStorage.js).
load() may return a Promise. On mount the graph is loaded from it, in its
saved view (or fitted, if the document has none); after that every edit, pan
and zoom is saved once edits settle for 400ms, and straight away when the page
is hidden or closed. Without storage, the editor starts empty and keeps nothing.
doc is the pipeline document from the spec (§12), plus the view, built
by graphDocument.js:
{
"format": "pipeline",
"nodes": [{ "id": "orders", "kind": "sql-query", "label": "Orders", "position": { "x": 0, "y": 0 } }],
"edges": [{ "id": "…", "source": "orders", "sourceHandle": "source-0", "target": "…", "targetHandle": "target-0" }],
"viewport": { "x": 415, "y": 355, "zoom": 1 }
}
A subnet node keeps its graph in its data (subnet: { nodes, edges, viewport }, the same shape), or names a shared one (subnetRef), which the
document keeps under subnets, by name, only when there are any (see
Subnets). A document without subnets saves exactly as before.
Loading drops nodes with no ID, kind or position, and wires to nodes that
aren't there, so stale or hand-edited documents still open. A node of a kind
this app doesn't have (made in another app, or with a plugin this app doesn't
install, or a kind a server supplies, while it isn't connected) is kept as a
placeholder: greyed, labelled with its kind's name, saying "Not available
here." Once the kind is defined (the server connects, say), it's a node like
any other. It has its first pin on each side plus any its
wires use, and it keeps its settings and wires, so saving the graph here loses
nothing. It can be moved, renamed or deleted, but not run (running it, or
anything downstream, fails with a message saying why), and new wires can't be
drawn to or from it. DAVY JONES' LOCKER's app shell supplies
one (createLocalGraphStorage(name)), which keeps the document in
localStorage under <name>:open-graph.
Running nodes
Everything runs in the browser, except PythonScript nodes, SQL that reads the backend database, and the kinds the server supplies, which run on the server engine (DAVY JONES' LOCKER's, in the framework). The engine can also run whole graphs, for scheduled runs.
FlowgraphEditor's server prop is the server engine as the host app sees it:
status reactive { state, address, local, assistant }: state
`checking`, `connected` or `unavailable`; local
true for a server in local development mode;
assistant { model, sampleRows } or null
query({ sql, inputs }) -> Promise<Uint8Array> the result, as Parquet
runPython({ code, inputs }) -> Promise<{ output, value?, variables?, error?, table? }>
runNode({ kind, node, inputs, files })
-> Promise<{ output, value?, error?, outputs }> (optional)
runLoop({ node, path, subnets, inputs, progress })
-> Promise<{ warnings, error?, outputs }> (optional)
assist({ kind, request, code, inputs })
-> Promise<{ code, note, model }> (optional)
inputs is { name: Parquet bytes }, named by reference name (or
name.table). runNode runs a node of a kind the server supplies (below):
its inputs are [{ ref, id, slot, pin, bytes }] (or with tables: [{ name, bytes }]), files its file fields' files [{ key, bytes }], and outputs
what it made, [{ slot, bytes }] or with tables, slot null for its first.
runLoop runs a Loop node on the server (see Loops): node its data, path
its path, subnets the document's shared subnets, inputs [{ pin, bytes }]
(item for items), progress({ done, total, failed }) called as its runs
go, and outputs [{ slot, bytes }]. DAVY JONES' LOCKER's app shell supplies one
(createEngineClient({ url, name })), which asks the engine's GET /health
for { status: 'ok', host?, port?, local? } every 5 s while it answers, every 15 s
while it doesn't (and not while the page is hidden). What it changes:
- Bottom left, in the window's edge gap: "LIVE · connected to server
host:port" in PythonScript's green, or "backend not available, client
execution only" in SQLQuery's blue. Its dot blinks (at 2 Hz) only while the
server is connected and the graph has a node that runs on it, so a blink
always means some data will go to the server; otherwise it's steady.
"· LOCAL" follows the address when the server is in local development mode
(
status.local: it signs no one in, and node kinds that run on it can reach the network), and the app's log in the Terminal warns so on connecting. - A node that runs on the server shows "Backend not available." as its status
while it's away, and a blinking dot, top right in its label's colour
("Executes on server"), while it's there. Manage notes that running it
sends its input data to the server, and Run waits for the server. That's
PythonScript (
where: 'server'in nodeKinds.js), and a SQLQuery whose query reads a table no wired input supplies (see below).
FlowgraphEditor takes a sql prop from the host app, the engine nodes run on:
query(sql) -> Promise<{ columns, rows }> plain values in the rows
registerFile(name, bytes) / readFile(name) / dropFile(name)
files its SQL reads and writes by name
readSqlite(bytes) / writeSqlite(tables, { statements }) SQLite files (optional)
loadSpatial() load DuckDB's spatial extension (optional)
The runner calls loadSpatial before reading Parquet that holds geometry
(GeoParquet geo metadata). Read without the extension, a geometry
column's CRS stays spelt out in full (PROJJSON), and DuckDB then treats it as
a different CRS from the same one by name (EPSG:4326). The table still
loads if the extension doesn't; only spatial SQL on it fails.
DAVY JONES' LOCKER's app shell supplies duckdbSql: DuckDB-wasm, with
sql.js for SQLite (DuckDB's sqlite extension can't reach browser files), both
imported from jsDelivr when first needed and never bundled. Its spatial
extension comes from the app's own origin, loaded when first needed. It turns
Arrow's values into plain ones: BIGINTs to numbers, dates and timestamps to
ISO text, decimals to scaled text.
graphRunner.js does the rest, in DuckDB SQL:
-
Run (Manage, or Ctrl+Enter in the SQL box) runs a node and everything upstream of it, in dependency order. Execute Graph (Flowgraphs) runs every node with Auto Run on, and their upstream. Runs queue, one at a time.
-
Each node's output is a table in the
flow_outschema. While a node runs,flow_inholds one view per node wired into it, named by that node's reference name (sqlnode1_data), and the search path isflow_in, main: a query sees its inputs and nothing else. A node with several tables (a workbook's sheets) is wired in as a schema of that name instead:dataingest1_data.sheet1. All of it is rebuilt every run. -
SQLQuery: the query, as a table. There's no switch between the database and the inputs: DuckDB's parser lists the tables a query reads (
serverTables, kept on the node assqlServerTableswhenever the query or its wires change), and if each is a wired input (or a CTE), the query runs here. If any isn't, it runs on the server instead, with its inputs sent as Parquet: there, plain names are the inputs, then the backend database'spublicschema (db.public.ordersin full). Manage names the tables it's going to the server for. -
PythonScript runs on the server. Each node wired in is a pandas DataFrame named by its reference name (several tables: an object with one per table). The last line, if it's an expression, is the node's value: a DataFrame (or Series, or Arrow table) becomes its table; anything else is shown in the Data panel (pretty-printed JSON, or its
repr), and the node has no rows.print()output goes to Terminal, as does the traceback, with the script's own lines only. Ctrl+Enter in the editor saves and runs. Geometry travels as GeoParquet, so where the server's runner has geopandas, an input with geometry is a GeoDataFrame with each column's CRS, and a GeoDataFrame (or GeoSeries) returned comes back asGEOMETRYwith its CRS; without geopandas, geometry is WKB bytes (DAVY JONES' LOCKER's engine README, "Python and geometry"). -
DataIngest reads its file when it's chosen, not when it runs: CSV, TSV, JSON, Parquet, Excel (a table per sheet, first row as headers, values not formulas), SQLite (a table per table), or a geo file (below). Each table is kept as Parquet in the host's storage (
putFile/getFile/deleteFile; IndexedDB in DAVY JONES' LOCKER's), and the file itself isn't kept. File Type defaults to the extension; changing it re-reads the file if it was chosen this session, else asks for it again. -
Geo files: GeoJSON, KML, a zipped shapefile (one or more
.shpwith their.shx,.dbfand.prj), GeoPackage and FlatGeobuf, read with DuckDB's spatial extension (ST_Read). Each layer is a table: one layer is the node's table, and several (a KML's folders, a GeoPackage's layers) are several tables, as a workbook's sheets are. Holes, multi-part features and points are kept as they are. A feature's geometry is aGEOMETRYcolumn calledgeometry(unless the file has a column of that name), with its CRS in the type: KML and GeoJSONEPSG:4326, a shapefile's from its.prj, a GeoPackage's per layer. A shapefile without a.prjhas none (plainGEOMETRY), and stays that way: nothing assumes longitude and latitude; nor does a GeoPackage's "undefined" SRS (srs_id-1 or 0), which GDAL reports as a CRS of that name. Heights that are all 0 (KML writes them) are dropped, so a geometry is 3D only when its heights mean something. GDAL's own feature numbers (OGC_FID) are left out. In SQL, spatial functions work with x, y axis order whatever the CRS says (ST_Transform(geometry, 'EPSG:7855')needs noalways_xy), as on the engine. A GeoPackage without GDAL's feature counts (gpkg_ogr_contents) can't be read here: DuckDB-wasm's GDAL then reads it on a thread, which it can't start. Graphs from before 0.4 keep what they read then: a GeoJSON's geometry as GeoJSON text. -
Several tables (Excel and SQLite, even with one sheet or table): each is named by its sheet or table name as an identifier (
toIdentifier: lower case, each run of anything but letters and digits as one_, none at either end,_before a leading digit;tableif nothing's left), then numbered if that's taken (sales,sales1):Q1 Salesisq1_sales,2024is_2024. Downstream it's<reference name>.<table>everywhere: a schema in SQL, in the browser and on the engine (SELECT * FROM dataingest1_data.q1_sales; the reference name alone isn't a table); attributes of an object in Python (getattrfor a keyword likeclass); properties of an object in JavaScript; DataExport's Input; the engine'sinputfilenames. Manage lists the first three under the node's ID, and a Data panel tab whose sheet name differs shows the table's name on hover. The names are fixed when the file is read: choosing a file whose sheets were renamed renames the tables, and code naming the old ones fails. -
DataExport writes a node wired into it to a file, as its File Type or the filename's extension (CSV if neither): CSV, TSV, JSON (geometry as WKT), Parquet (GeoParquet with geometry), Excel (one table), SQLite (every table; geometry as WKT, blobs as blobs), or a geo format (geoExport.js):
- GeoJSON (one table) and KML (every table, a folder each):
longitude and latitude by their standards, so geometry is transformed
from its CRS, and refused when it has none. KML takes
nameanddescriptioncolumns as each placemark's own, and the rest as ExtendedData. - GeoPackage (every table, a layer each; one without geometry an
attributes table) and Shapefile (.zip) (every table, a shapefile
each): the CRS kept, its WKT and EPSG code as GDAL reports them, and
none when it's unknown (
srs_id-1, no.prj). A shapefile holds one geometry column of one kind (points, lines or polygons), with its rings turned the way shapefiles want them; its dBase fields are cut to 10 characters in the name and 254 bytes of text (UTF-8, as the.cpgsays).
A table's geometry is its column called
geometry, or else its first. They're assembled in JavaScript, since DuckDB-wasm's GDAL corrupts what it writes. With one node wired in, that one; with several, the one named under Input (shown only then): its reference name, its ID, or<name>.<table>for one table of several, typed or chosen from a list (an editor menu, badged by category, like the right-click ones; Alt+Down opens it). It downloads when the node itself is run, not when it runs upstream of another, and passes the input on unchanged. Parquet keeps each geometry column's CRS (below). DuckDB-wasm's xlsx files come out with a stray byte ahead of the zip, which Excel calls corrupt;repairXlsxcuts it (and leaves a correct file alone). - GeoJSON (one table) and KML (every table, a folder each):
longitude and latitude by their standards, so geometry is transformed
from its CRS, and refused when it has none. KML takes
-
JavaScript runs its code in jsSandbox.js: a worker inside a hidden
sandbox="allow-scripts"iframe. Its origin is opaque, so the code can't reach the page, its storage or its cookies (shared graphs are safe to run), and it's stopped after 30 s. The code is an async function body; each node wired in is a variable named by its reference name (an array of row objects, or an object of them for several tables). Returning an array of rows makes its table;console.log/printlines are its output (Terminal);await sleep(s)waits. It never runs on the server (where: 'browser') until the engine has safeguards for user code. -
Other kinds fail with "… nodes can't run yet.", and anything downstream of a failure fails too.
-
Results are pages of 100 rows plus a count; the Data panel's pager re-runs the node for another page. Clicking a row picks it (highlighted; click it again to unpick), for panels that follow it, like the Map panel; picking one elsewhere (a feature on the map) turns Data to its page and scrolls to it.
-
Spatial warnings (spatialChecks.js): a SQLQuery that runs but likely doesn't mean what it says gets a warning (the run's
warnings), and so does DataExport's GeoJSON and KML:- Measuring in degrees:
ST_Area,ST_Length,ST_Perimeter,ST_Distance,ST_DWithinorST_Bufferon geometry in a longitude and latitude CRS, pointing to the_Spheroidfunction or a projected CRS. - A dropped datum shift:
ST_Transformbetween datums PROJ joins only by a null transformation (WGS 84 ↔ GDA2020, GDA94 ↔ WGS 84, NAD27 ↔ NAD83 without grids), so coordinates are carried over unchanged. A GeoJSON or KML export from such a CRS too, since those formats are WGS 84. GDA94 → GDA2020, which PROJ shifts, doesn't warn. - A target with no datum:
ST_Transformto a PROJ string that names only an ellipsoid (+proj=tmerc ... +ellps=GRS80, no+datum,+towgs84or+nadgrids), constant or built per row. PROJ applies no datum shift, so the result stays on the source's datum, which the warning names. It can't know which datum the projection was meant for, so it warns even when the source is already on it: naming the target in WKT or PROJJSON, which carry their datum, silences it (and from another datum, PROJ then shifts properly). - The
_Spheroidfunctions given the wrong coordinates: withST_FlipCoordinates(they take longitude first here, so flipped they return NaN, silently), or on a projected CRS.
The calls come from DuckDB's parse of the query, and each geometry argument's type (its CRS) from DuckDB describing that argument selected from the same FROM. A null transformation is found by transforming a point in the CRS's area of use between the two geographic CRSs and seeing it not move (crs.js), since DuckDB's spatial doesn't say which operation PROJ picks or how accurate it's rated. What can't be worked out (a server table's types, an alias from the same SELECT) gets no warning; a check never stops a run.
- Measuring in degrees:
-
Geometry in Data: a geometry column's header has its types and CRS (
POLYGON · EPSG:4326, or "CRS unknown"), and how many of its geometries are invalid or empty (in amber). A cell says what its geometry is, not its bytes: its type, and its area or length on the ellipsoid (POLYGON · 1.75 ha,LINESTRING · 550 m), with its WKT on hover. One whose CRS isn't known shows its type alone; without the spatial extension, cells show WKT. -
A geometry's CRS travels with its table. In SQL it's on the column's type (
GEOMETRY('EPSG:7855')): a column passed through keeps its CRS,ST_Transformgives its target's, and mixing CRSs is refused ("Cannot call function 'ST_Intersects' with geometries of different coordinate reference systems"). On the wire it's GeoParquetgeometadata, written by DuckDB whenever a table goes out as Parquet (to the server, to PythonScript, to a server kind, from DataExport, and DataIngest's stored tables), and read back onto the type when one comes in. Two exceptions to DuckDB's own handling:- No CRS stays no CRS. DuckDB leaves
crsout for a column with none, and GeoParquet reads that as OGC:CRS84 (longitude and latitude), so the runner writes such a file again with"crs": null(writeParquet). - OGC:CRS84 is read as
EPSG:4326, the same coordinates in x, y order. DuckDB-wasm's spatial extension fails on CRS84 ("stoi: no conversion"), even opening a file that has it, so the runner readsgeoitself, with DuckDB's GeoParquet reading off (readParquet). The engine does the same, so both give a column the same type.
- No CRS stays no CRS. DuckDB leaves
Run state lives in the graph store (run), not in the saved document:
statuses and results start empty whenever a graph is opened. A status is
running, completed, failed or stale: changing anything that affects a
node's output (its SQL, file, format or output name; a wire into it; a rename
upstream) makes it and whatever ran downstream Stale, and a node edited
while it runs comes back stale. Reset (Manage, or the node's menu) takes a
node back to not run. Nodes show their status top left, and wires take their
colours from it (see the roadmap's wire table; stale counts as not run).
Unavailable buttons use aria-disabled rather than disabled, which would
drop keyboard focus, and with it the editor's hotkeys.
Limits of files
Everything known not to work, or to change data on the way, in one place (most of it from DuckDB-wasm and its GDAL; ROADMAP's 8g has the findings).
Reading (DataIngest)
- Not read: KMZ (zipped KML), File Geodatabase (the browser's GDAL has
no OpenFileGDB driver), and formats GDAL has but DataIngest doesn't offer
yet (GML, GPX, DXF, MapInfo). A shapefile must come zipped, with its
.shxand.dbf; a bare.shpisn't read. - Zips: stored or deflated members only; no Zip64 (over 4 GB) and no
encrypted members.
__MACOSX/is skipped, and a zip with no.shpis refused. - GeoPackage needs GDAL's feature counts (
gpkg_ogr_contents): without them DuckDB-wasm's GDAL reads on a thread, which it can't start ("thread constructor failed"). GDAL, QGIS and DataExport write them; saving the file again with QGIS or ogr2ogr adds them. Its "undefined" SRS (srs_id-1 or 0) is read as no CRS. - KML: each placemark's
nameanddescriptiononly. GDAL's KML reader skips ExtendedData, so other fields (DataExport's included) don't come back. Heights that are all 0 are dropped. - Shapefile without a
.prj: no CRS. - GeoParquet: only WKB columns are geometry; GeoParquet 1.1's native
encodings (
point,polygon...) stay as they are. Nocrsmeans OGC:CRS84, read asEPSG:4326; an explicit null means no CRS. - Geo files need the spatial extension, served by the app (DAVY JONES' LOCKER's README, "DuckDB's extensions").
- Excel: the first row is the column names; values, not formulas.
- SQLite: blobs become
\x…hex text. - GDAL's feature numbers (
OGC_FID) are left out.
Writing (DataExport)
- One table: CSV, TSV, JSON, Parquet, Excel, GeoJSON. Every table: SQLite, KML, GeoPackage, Shapefile (.zip).
- CSV, TSV, JSON, Excel and SQLite write geometry as WKT, without its CRS. SQLite keeps blobs as blobs, and writes lists and structs as JSON text.
- GeoJSON and KML are longitude and latitude only: geometry is
transformed, and a column with no CRS is refused. KML heights are written
with no
altitudeMode, so viewers put them on the ground. - GeoPackage: no spatial index (R-tree); a layer of mixed types is
GEOMETRY; one geometry column per layer, any others as WKT text; timestamps, lists and structs as text; a CRS without an EPSG code gets ansrs_idfrom 100000. - Shapefile: one geometry column of one kind (points, lines or
polygons), or it's refused; no geometry collections; M values dropped;
an empty geometry becomes a null shape; field names cut to 10 ASCII
characters (others become
_), text to 254 bytes, reals to 15 decimals; timestamps and lists as text. The.prjis OGC WKT 1, which GDAL and QGIS read; ArcGIS writes its own flavour. - The browser's GDAL writers aren't used (they corrupt what they write in DuckDB-wasm), so these formats are SEAMONSTER's own writers.
- On the engine (whole-graph runs), DataExport writes CSV, TSV, JSON and Parquet only, and its CSV quotes every field.
In SQL
- In the browser, use
EPSG:4326, notOGC:CRS84: DuckDB-wasm's spatial fails on CRS84 ("stoi: no conversion"); the two are the same coordinates in Flow's x, y order. - No datum grids in either engine: a transformation that needs one (GDA94 ↔ GDA2020 by grid, NAD27 ↔ NAD83) uses PROJ's grid-free choice, the same in both.
- The engine reads no files (
ST_Readand the other readers are refused): read them with DataIngest and wire them in. - The
_Spheroidfunctions take longitude first (x, y), as everything here does:ST_Area_Spheroid(geometry)onEPSG:4326orEPSG:7844is right as it is.ST_FlipCoordinatesbefore them, which plain DuckDB withoutgeometry_always_xyneeds, gives NaN (a warning says so). ST_Transform(geometry, source, target)returns no CRS: the explicit source wins over the column's, but the result is plainGEOMETRY, so later nodes see "CRS unknown" (GeoJSON and KML exports refuse it).ST_Transform(geometry, target)on a column with its CRS keeps one; useST_SetCRSafter the three-argument form to put it back.- A target that isn't a fixed authority code (a PROJ string, WKT, or
anything built per row) works only in the three-argument form, on
geometry with no CRS on its type (
ST_GeomFromWKBof the bytes, say). On a column with its CRS, the target must be a constant DuckDB recognises ('EPSG:7855'); a PROJ string there is refused ("not a recognized coordinate reference system"). - Spatial warnings are worked out in the browser, for SQL run from the editor (here or on the server); a whole-graph run on the engine has none. They can't name PROJ's chosen operation or its rated accuracy, only that the datum shift is dropped, and a transformation that does shift, but is rated at 1 m or worse, gets none. Geometry whose type can't be worked out (a server table, an alias from the same SELECT) isn't checked.
On the map (the Map panel)
- A geometry column with no CRS isn't drawn ("CRS unknown"): give it one
with
ST_SetCRS. One PROJ can't transform to longitude and latitude isn't drawn either, and the panel says why. - The first 10,000 rows of each geometry column are drawn (the
featureLimitprop), and the first 200 rows of a layers table; a note says when there are more. A layers table'sdatais drawn as GeoJSON is, in longitude and latitude, whatever its source was. - Leaflet only: there's no deck.gl Map panel yet, for more features than Leaflet draws smoothly.
- Each run reads every output table's geometry as GeoJSON while a Map panel is there, so runs with large geometry tables take longer.
- A node that ran before the Map panel was mounted isn't drawn until it runs again (results aren't saved, so this is only when a host adds the panel while the editor is open).
- The map's view isn't saved with the graph: an opened graph fits its first layers again.
In Python (PythonScript)
- Geometry is a GeoDataFrame only where the server's runner has geopandas (the framework's runner doesn't: an app adds it). Without it, geometry arrives as WKB bytes, and a DataFrame returned with those bytes comes back as a blob column, not geometry, with no CRS.
- Return geometry as a GeoDataFrame or GeoSeries, with its CRS set
(
set_crsorto_crs). A GeoDataFrame'scrsof None comes back as no CRS. Shapely objects in a plain column (not a GeoSeries) can't be returned, and the run fails. - GeoParquet's native encodings (GeoParquet 1.1's
point,polygon...) aren't read as geometry; geopandas writes WKB, which is.
Subnets
A Subnet node (Custom) is a graph of its own, run as one node (roadmap 10c, subnets.js). A new one starts as an Input wired to an Output, so it's one in, one out, and passes its input through.
- Opening one: double-click it, press Enter with it selected, Open subnet
in Manage, or its menu. Its graph takes the canvas, with a breadcrumb after
the title (
Graph › Screening); Escape or the breadcrumb goes back up, with the subnet selected. Everything works inside as at the top: the Library and menus (which add Input and Output there, under Subnet), Manage, Data, the Terminal, running, undo (each level its own). - Pins: each Input node inside is an input pin on the subnet node, and
each Output node an output, named by the node's ID, top to bottom (then
left to right). An Input pin takes one wire, and the Input puts out the
first slot it carries. The topmost Output is the subnet's first slot, read
downstream as
<subnet id>_<output id>; any others are slots of their own, each with its own pin too, and the main output pin carries them all. Renaming an Input or Output renames its pin, and the wires to it follow; deleting one drops the wires to its pin. - Running: the subnet's graph runs as it is, not flattened, with what's
wired into its pins put out by its Input nodes. Inside, nodes are known by
path (
screening/classify): their results and tables are kept so, and each shows its status and output when the subnet is open. Running a node inside runs what's upstream of it at every level (the subnet's inputs first), but not the rest of the subnet. An error says where it started:Screening › Classify: …. Subnets nest, up to 8 deep (a shared subnet that contains itself stops there). - Stale: an edit inside makes the subnet stale, with what's downstream of it, at every level up; changing what's wired into a subnet makes everything inside it stale.
- Shared subnets: Manage's Graph shares a subnet's graph under a name
(the document keeps it under
subnets), so other Subnet nodes can use the same graph (Graph > Shared: name), or take a copy of their own. Editing a shared graph changes every use, and each one goes stale.
The graph store has it as API: levels (the levels open: { depth, label, shared }, the top first), openSubnet(id) (returns an error message or
null), closeSubnet(depth?) (one up by default), library (the shared
graphs), shareSubnet(id, name) and useSubnet(id, name | null). Its
other methods take the open level's IDs; run.status, run.results and
logs.nodes show the open level's nodes by ID, and the runner and the
store keep them by path. DAVY JONES' LOCKER's engine runs subnets the
same way (its README, "API").
Known limits:
- The open level isn't saved: reopening the page shows the top level.
- Deleting an Input or Output drops its subnet's wires to that pin (every use, for a shared one), and undoing the deletion brings the node back, not those wires. Moving another Output above the first changes which one is first: wires from the new first one's own pin go.
- Renaming an Input or Output doesn't rewrite code that reads it
(
site_data), as for any node. - Shared subnets no one uses stay in the document; there's no way to delete one yet.
Loops
A Loop node (Custom) is a subnet run once per row (roadmap 10d): screen each of 500 sites, say. Everything under Subnets holds for it (opening it, its pins, sharing its graph), and:
- Its Input node
itemis its items pin: wire a table into it, and the loop runs its graph once per row,itemputting out that row (a one-row table). Its other Inputs put out the same table every run. A new Loop starts asitemwired to an Output, so each run passes its row through. - Each Output gathers every run's table, by column name, with
_keyand_iterationfirst:_keyis the run's row's value in Key column, or its row number;_iterationcounts runs from 1. A failed run adds no rows. An Output takes one table (not a workbook's several). <id>_status, an output of its own (with its own pin): a row per run,_key,_iteration,status(completed, failed or skipped),error(where it started:Classify: …),duration_msandrows(each Output's row count, as JSON). An Output inside can't be calledstatus.- Settings: Key column; When a run fails (carry on with the rest, the default, which warns how many failed; or stop the loop, which fails it, naming the run); Most runs (1000: rows past it aren't run, with a warning); Runs at once (on the server only).
- Where it runs: in the browser, one run at a time, unless something
inside runs only on the server (PythonScript, SQL reading the server's
tables, the server's own kinds, a subnet or loop with one of those inside).
Then the server runs the whole loop, as a job (
server.runLoop), with as many runs at once as Runs at once asks (up to the engine'sLOOP_MAX_CONCURRENCY). A loop with nodes only the server runs and JavaScript, which only the browser runs, can't run, and says so. The node blinks its dot when it'll run on the server, as far as its own graph shows (a shared graph, or one inside, is worked out when it runs). - Progress: the node's face counts runs while it goes, "Running… 37 /
500 · 2 failed" (
run.progresson the graph store, by ID). - A node inside run on its own runs in the first run (the first row), in the browser, so you can see what a run does.
Known limits:
- Opened after a run, a loop shows its last run's nodes (in the
browser), or none when the server ran it: the server sends back the
outputs, not what each run's nodes did.
statussays how each run went. - No resume yet: running a loop again runs every row again.
- In the browser, one run at a time; on the server, PythonScript inside runs one at a time whatever Runs at once says (the runner runs one script at a time). Keep Runs at once at 1 for loops of slow scripts, or their waits can outlast the engine's run timeout.
- A loop the server runs takes one table per input: a workbook's sheets need picking first (a SQLQuery node).
Defining node kinds
Node kinds live in a registry. SEAMONSTER's own are defined in it
(builtinKinds.js) exactly as an app defines its own,
with defineNodeKind and defineNodeCategory, both exported. Define them
before FlowgraphEditor mounts, as themes are:
import { defineNodeCategory, defineNodeKind } from 'seamonster'
defineNodeCategory({ id: 'geo', label: 'Geo', colors: { from: '#7dd3a8', to: '#2f855a' } })
defineNodeKind({
kind: 'geo.double', // stored in saved graphs; namespace an app's or plugin's with a dot
label: 'Double', // the name on its face, in the Library and in the menus
category: 'geo', // a built-in category's id, or one defined as above
outputName: true, // Manage offers Output Name
async run(ctx) {
const [input] = ctx.inputs
if (!input) return { error: 'Wire a node in.' }
const table = ctx.table()
await ctx.sql.query(`CREATE TABLE ${table} AS SELECT n * 2 AS n2 FROM ${input.table}`)
return { table, output: `doubled ${input.ref}` }
},
})
It appears in the Library, the canvas menu and the menu for a wire dropped
on the canvas, runs, and is saved and loaded like any node. The registry is
reactive, so a kind defined after mounting appears too. A category's
colors are its face's gradient (from, to) and, optionally, its name's
colour (label, white by default).
A kind's definition. Only kind, label and category are needed.
kind |
lowercase letters, digits and hyphens, with one dot at most (sql-query, geo.double) |
description |
what it does, in a sentence or two: Manage shows it above the node's fields, and the Library when its card is hovered |
idPrefix |
new nodes' IDs are <idPrefix><n>; by default the label, lowercase without spaces (double1) |
run(ctx) |
runs it in the browser; without it, the node says it can't run yet |
runs |
whether Manage shows Auto Run and Run; by default, whether there's a run |
where |
'server' (it runs only there, and says so), 'browser' (never on the server), or a function of the node's data returning either (SQLQuery's). Unset: the browser. A kind with where: 'server' and no run runs through the server prop's runNode: that's how the kinds a server supplies run (DAVY JONES' LOCKER defines them from its engine's GET /nodes) |
outputName |
whether Manage offers Output Name |
fields |
what Manage shows for it, in order (below), each kept on the node's data under its key |
canRun(data) |
a reason it can't run yet (shown under Run), or null |
runHint |
the line under Run (where results go, by default) |
slots |
its outputs beyond the first, [{ name, label?, pin? }] or a function of the node's data (below) |
inputPins |
named input pins beside the main one, [{ name, label?, many?, hint?, required? }] or a function of the data (below) |
mainInput |
the main input pin: 'many' wires (the default), 'one', or false for none (a Subnet's inputs are all named pins) |
mainOutput |
false for no main output pin (an Output node, whose output leaves its subnet instead) |
create() |
data a new node of the kind starts with (a Subnet's graph) |
ctx, what run is given:
id, node |
the node's ID, and its data (kind, label and its own fields, with their defaults) |
inputs |
what's wired in, one per slot each wire carries: [{ id, kind, slot, ref, pin, table }], or with tables: [{ name, label, table }] for one with several. id is the node it's from, slot null for that node's first slot, ref the reference name it's read by, pin null for the main input pin or the named pin it came through, table a table name for SQL. The search path also finds each input by ref |
sql |
the host's SQL engine (query, registerFile, readFile, dropFile) |
server |
the server: { state, query?, runPython? }, as FlowgraphEditor's server |
getFile(key) |
bytes the host stored by key |
table(name?) |
a name for this node's output table (one of several, with a name): create it, then return it |
slotTable(slot, name?) |
the same, for one of its other slots |
rows(input) |
an input's rows as objects by column (an object of them by table name, for several) |
parquet() |
the inputs as Parquet, [{ name, bytes }], named as the node reads them, for sending to a server |
parquetOf(table) |
one table as Parquet (bytes) |
loadParquet(table, bytes), loadRows(table, rows) |
create a table from Parquet, or from rows |
serverTables(query) |
the tables a query reads that no input supplies |
spatialWarnings(query) |
warnings about a query's spatial SQL (see "Spatial warnings"), [text]; never throws |
runSubnet(), runLoop(), boundInput |
the built-in Subnet, Loop and Input kinds' (see Subnets, Loops) |
What run returns (or throws, for an error):
table |
its output table; or tables: [{ name, label, table }] for several, shown as tabs in Data and read downstream as <ref>.<name> |
slots |
its other slots: { [slot]: table }, or { [slot]: { tables } }; a slot left out is an empty table |
value |
a value that isn't a table, shown in Data. With no table, downstream reads an empty one |
export |
{ filename, contentType, size, bytes }: a file, saved when this node is the one run |
output |
what it printed, for the Terminal |
warnings |
[text]: it ran, but something's likely wrong. In the Terminal (amber), and on the node's badge while that run is current |
error |
why it failed |
Anything else it returns stays on the result (PythonScript's variables).
A saved graph naming a kind this app doesn't have keeps the node as a
placeholder (see Keeping the open graph).
Fields. Manage shows a node's ID and Name, then its kind's fields,
then Output Name and Run. Text, number and code fields hold a draft and
commit on blur or Enter (Ctrl+Enter in code also runs the node); the rest
commit straight away. The built-ins declare theirs the same way.
fields: [
{ key: 'crs', label: 'Output CRS', type: 'text', mono: true, default: 'EPSG:4326',
validate: [{ pattern: '^EPSG:\\d+$', message: 'Use EPSG:<code>.' }] },
{ key: 'tolerance', label: 'Tolerance (m)', type: 'number', default: 3, min: 0,
validate: [{ min: 2.5, severity: 'warn', message: 'Smaller than the datum offsets.' }] },
{ key: 'mode', label: 'Mode', type: 'select', options: [{ value: 'fast', label: 'Fast' }, { value: 'exact', label: 'Exact' }] },
{ key: 'source', label: 'Source', type: 'file', accept: '.kml,.geojson' },
]
| Type | Its own options |
|---|---|
text |
placeholder, mono |
number |
placeholder, min, max, step |
select |
options: [{ value, label }], or a function of the node's data; '' is stored as nothing |
checkbox |
|
code |
language (sql, python, javascript, text), placeholder, rows; assistant: true puts the AI assistant above it (the server's assistant writes code for SQLQuery, PythonScript and JavaScript) |
file |
accept. The file is stored through the host's storage and the field holds { fileName, fileSize, key }; run reads it with ctx.getFile(key) |
readonly |
value(data): text to show |
custom |
component: a Vue component given node, for what no type does (DataIngest's file chooser) |
Every field can also have default (its value until one is set; run sees
it too), hint (a line under it, with `backticks` for code, or a
function (data, typed) => text), affectsOutput: false (editing it
doesn't make the node stale) and visible(data).
Rules. A field's validate is a list of rules, each with a message,
an optional severity ('error', the default, or 'warn') and one test:
required: true, min, max, pattern (a regex, for text), or when: (value, data) => true when it's wrong. Rules are checked as a field is
edited. An error shows under the field, stops the node running (Run says
why), and is its status on the canvas until it has run; a warning shows under
the field and as a badge on the node, whose tooltip says what it is. Rules
other than when are plain data, so a kind the server defines can carry
them.
Slots and pins. A node's outputs are slots. The first is what run
returns as its table, read downstream by the node's reference name
(<id>_data, or <id>_<Output Name>). A kind's slots add more, each read
as <id>_<slot> and filled through slots in what run returns:
defineNodeKind({
kind: 'geo.classify', label: 'Classify', category: 'geo',
slots: [{ name: 'summary' }, { name: 'status', pin: true }],
async run(ctx) {
const table = ctx.table() // classify_data
const summary = ctx.slotTable('summary') // classify_summary
// ... create both ...
return { table, slots: { summary } } // status: left out, so an empty table
},
})
- One output pin carries every slot. Wiring it gives the node downstream all of them, each by its reference name; hovering it lists them.
- A slot can have a pin of its own, carrying just that slot: when its
kind says
pin: true, or when the node's data lists the slot inslotPins(the user's choice; step 9 adds the controls). The main pin still carries it, so giving a slot a pin never breaks a wire. Named pins show their names while the node is hovered. - Inputs come through the main input pin, any number of wires. A kind's
inputPinsadd named ones for inputs with a role (ctx.inputs[i].pinsays which pin each came through); a named pin takes one wire unless it saysmany: true. Users don't add input pins. A pin'shintshows when it's hovered, after its label ("Site: the boundary to summarise around; optional"). Arequired: truepin with nothing wired in stops the node running: its ring shows hollow red even when the node isn't hovered, its face and Manage say "Site: wire a node into this input first.", Run is refused, and so is a run from downstream. Pins are optional by default. - A node of a kind this app doesn't have (a placeholder) keeps every wire it was saved with, into named input pins as well as out of named slots.
- Handles: the main pins are
source-0andtarget-0, as before, so saved graphs load unchanged; the others are named,source:<slot>andtarget:<name>, so reordering pins never rewires a graph. - Shown: Data has a tab per slot, named as downstream code reads it; the Terminal lists the other slots' sizes after a run. Output Name can't take the name of another of the node's slots.
- A saved graph's wires carry their handles, and the server's whole-graph
runs (
/execute) read them as the browser does: a wire from the main pin brings every slot, one from a slot's own pin (source:<slot>) just that slot, and one into a named input pin (target:<name>) says so in the input'spin. There, only the kinds the server defines have slots beyond the first (the engine's README, "The app's node kinds"). - One wire can bring a named input pin several tables. A named pin
takes one wire, not one table, so a wire from a node's main pin brings
every slot of that node through it, each with that
pin. A kind that wants one table there picks it (by slot, or by what's in it), or the user wires the slot's own pin instead.
AI assistant
SQLQuery, PythonScript and JavaScript nodes have an Assistant field in
Manage, above their code, while the server is there with its assistant set
up (status.assistant; the engine's README has the LiteLLM settings).
Type what the code should do and press Enter (Shift+Enter for a new line):
- What's upstream runs, without changing any statuses, and each input is
described: its reference name, columns and types, row count, and first
sampleRowsrows (graphRunner.js'sdescribeInputs). - The engine's
/assistgets the request, the node's kind and the code in its editor now (so "now sort it by name" changes what's there), with those inputs. For SQL, the engine adds the server database's tables and columns. - The answer replaces the node's code, which marks it stale like any edit, with the model's note underneath and Undo (while the code is still what it wrote). Nothing runs until the user runs it.
The field says what it sends, and to which model, before it's used.
Panels
A panel component is a thin wrapper around FlowPanel:
<FlowPanel class="manage-panel" name="manage" title="Manage" dock="right" />
Props: name (unique id), title, dock (left | right | bottom),
sizing (fill | content, below), defaultSize (px along the linked
axis), minWidth (200), minHeight (140), collapsed (initial), hotkey
(a single key), aboveBottom (see below). Panels register with the layout
of the PanelHost around them: FlowgraphEditor is a PanelHost, and any app
can use one to put the same panels over its own content (a map, a scene),
from seamonster/panels (widgets/panels). Panels
on one side sit side by side, from the edge inwards, in the order they
appear in the host's template. Names must be unique.
The editor's panels are Terminal, Library and Flowgraphs on the left, Data at
the bottom and Manage on the right. All but Manage start collapsed.
Flowgraphs is aboveBottom: it sits above Data, which runs underneath it.
(Terminal was called Console in 0.1; it was renamed so it isn't mistaken for
the browser's DevTools console, and its hotkey moved from C to T.)
A host's own panels go in FlowgraphEditor's panels slot (DAVY JONES'
LOCKER's DavyJonesLocker passes it through). Filled, the slot replaces the
editor's panels, so a host that adds to them puts EditorPanels (all five,
in their usual order) beside its own:
<script setup>
import { EditorPanels, FlowgraphEditor } from 'seamonster'
import { MapPanel } from 'seamonster/spatial/leaflet'
</script>
<template>
<FlowgraphEditor :storage="storage" :sql="sql">
<template #panels>
<EditorPanels />
<MapPanel dock="right" aboveBottom />
</template>
</FlowgraphEditor>
</template>
To move, reorder or leave out the editor's panels, list them one by one
instead: TerminalPanel, LibraryPanel, FlowgraphsPanel, DataPanel and
ManagePanel are exported too, and FlowPanel's props given to one replace
its own (<ManagePanel dock="left" />, <DataPanel :collapsed="false" />).
A panel of the host's own is a FlowPanel (above), or any component whose
root is one, like the Map panel.
A hotkey stays with the first panel to register it: a later one with the
same key gets none, and the console says so.
The graph store (FLOW_GRAPH, injected; PANEL_LAYOUT is the layout's) has
what a host's panels need to follow the editor:
selectedNode(a computed node, or null) andselect(id).run.results[id], a node's last result (see Running nodes).dataView: what Data shows of the selected node,{ node, slot, table, row }: the slot's and table's tabs open (null for the first) and the row picked (counting from 0 in the whole table; null for none). It goes back to the first tabs and no row when another node is selected.showRow({ id, slot, table, row }): picks a row in Data, selecting the node, opening its tabs and turning to the row's page (a run, like the pager's).addOutputReader(name, read):read({ table, columns, query })runs on every output table as its node runs, while the table is there (they're dropped by the next run):tableis its name to put in SQL,columns[{ name, type, crs }](crsundefined for a column that isn't geometry, null for geometry with none), andquery(sql)the SQL engine's. What it returns is kept beside the table's page asextras[name](or{ error }if it throws), held as it is rather than made reactive. A node that ran before the reader was added has nothing from it until it runs again. Returns a function that removes it.setPanelData(id, panel, value): a panel's own data on a node, in itsdata.panels[panel](null removes it). It's saved with the graph, follows the node through renames, deletion and undo, and leaves the node fresh, not stale.
The Terminal's logs. Each node has a log, and so does the app. The Terminal shows the selected node's, or the app's when nothing is selected, newest at the bottom.
- A run adds to its node's log: what a script printed, then its error or a summary ("Returned 250 rows."). Everything upstream that ran with it adds to theirs.
- A log belongs to its node's current state: its settings, its wires, anything upstream, and Reset. The first run after any of those changes clears the log before adding to it, so errors a change dealt with go away, even if the new run fails differently. Until then a stale node's log says it has changed since.
- An entry already in the log isn't added again: it moves to the end and counts the repeat (×2), so an unchanged failing node shows its error once.
- The app's log has the server connecting and going away, and Execute
Graph's summary. A host adds to it with the graph store's
log(text, type), type being'info'(the default),'error','warning'or'note'; the entries are inlogs.app, and each node's inlogs.nodes[id]. - Turning pages in Data doesn't log (it runs the node through
turnPage). Logs aren't saved, like run state.
The behaviour lives in panelLayout.js:
- Linked (the default): panels share the window. Left panels sit side by side from the left edge, right panels from the right, and the bottom panel fills the width between them. Each resizes on one axis only, from the edge facing the canvas. A linked panel only makes room for other linked panels.
- Above the bottom panel: the last panels on either side (the innermost)
can be
aboveBottom. While the bottom panel is open (and linked) they stop above it, and it extends underneath them, as far as the side's other panels, which stay full height at the edge. Resizing the bottom panel moves their bottom edge, down to their minimum height. With the bottom panel closed they're full height. - Unlinked (the link button, or Ctrl+Space on the active panel): the panel floats. It moves by dragging its title and resizes from every edge and corner. Its size and position are saved to localStorage, as fractions of the window, and restored the next time it's unlinked.
- Sizes are proportional, so panels scale with the window down to their minimums. If the window gets too narrow for the linked panels at their minimums, they unlink.
- Title click: collapses the panel, or only brings it to the front if another panel covers it. Clicking anywhere on a panel brings it to the front.
- Rails (side titles in the window-edge gap) show collapsed panels. On an
edge with several panels, once any of them is unlinked, the open ones show a
rail too, so a covered panel can still be reached. A rail click behaves like
a title click. With
autoHideRailsoff (a FlowgraphEditor prop), every panel keeps its rail, open or collapsed. - Hotkeys: a panel's own key (T, L, G, D, M; the Map panel's P) acts like a rail click
(and
~focuses the Terminal's command field, opening it): opens it, brings it to the front if it's covered, or else collapses it. Ignored with Ctrl, Cmd or Alt held, or while typing in a field. - Ctrl+Space with no panel active (after clicking the canvas) collapses all panels to maximise the canvas; pressing it again restores them. The window needs focus for any of these keys, so click inside it first.
- "Set unlinked size to current" (
resetUnlinkedRect) saves the panel's linked position as its unlinked one, without unlinking it. - Content panels (
sizing="content"), for panels floating over a host's content rather than sharing the window: linked, a side panel sits at the top of its edge,defaultSizewide (resizable), as tall as its content; a bottom panel sits at the left, as wide and tall as its content. They overlay everything else and don't push other panels. Once the window caps one, its body scrolls, across as well as down. Each shows its rail (at the start of its edge) while it overlaps another panel. Unlinked, they're like any other panel, minimum sizes included.
Context menus
Menus use the menu widget. PanelHost renders
the editor's root through MenuHost, and regions opt in with v-menu:
- Panel titles and rails:
PANEL_MENU, with the panel's name as context. - Canvas: Nodes (categories with colour badges, then node types) and Panels.
- Nodes and wires:
NODE_MENU(Open subnet, for a subnet; Reset; Delete) andWIRE_MENU(Delete), with the ID as context. - A wire dropped on empty canvas:
CONNECTION_MENU, opened from code withMenuHost'sopen().
Menus are defined as data: the panels' in panelMenus.js
(PANEL_MENU, panelCommands(layout), panelsSubmenu(layout)), the graph's
in flowMenus.js. PanelHost adds the panel
commands, and the editor its own on top. Right-clicking outside a region
shows no menu, not even the browser's.
The menu look is in a <style> block in PanelHost.vue.
It sets the --wm-* tokens on .flow-surface (every PanelHost, so the
editor too), plus the part-class overrides: the category-coloured badges, and
the panning gradient on highlighted items (category colours for items under a
badged category, blue-green otherwise). Menus are portalled into their host,
so this styling reaches only its menus.
Styling
styles.css: the editor's tokens (--flow-*) and every one of its rules. Layout comes entirely from CSS, and components carry no styles of their own except the menu block in PanelHost.vue.- A host app sizes FlowgraphEditor's container itself.
--flow-panel-radiusis both the panel corner radius and the gap between a docked panel and the window edge; the rails sit in that gap.- Panel titles and
Related Packages
An app framework for flowgraphs on SEAMONSTER: the browser shell (engine client,...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.4.2 | ^3.5.0 | Apache-2.0 | |
| 0.4.1 | ^3.5.0 | Apache-2.0 | |
| 0.4.0 | ^3.5.0 | Apache-2.0 | |
| 0.3.1 | ^3.5.0 | Apache-2.0 | |
| 0.3.0 | ^3.5.0 | Apache-2.0 | |
| 0.2.0 | ^3.5.0 | Apache-2.0 | |
| 0.1.0 | ^3.5.0 | Apache-2.0 |