flograph
A visual node-based Python environment: dataflow on an infinite Blueprint-style canvas, where every node is real, editable Python — and the same graph also gives you interactive dashboards and printable reports.
Build the pipeline on the model canvas, put its live results on a dashboard page for someone who will never open the model, and write the write-up on a report page that pulls the same charts and numbers into markdown and prints to PDF. One file, three surfaces, no export step between them.
Install
flograph is a standard pip-installable package (hatchling build backend):
pip install flograph # or, from a checkout: pip install -e .
That puts a flograph command on your PATH and makes python -m flograph
work. Optional extras pull in what individual nodes need:
| Extra | Brings | For |
|---|---|---|
matplotlib |
matplotlib | Show Plot, Chart per Value |
plotly |
plotly | Show Plotly, Chart per Value (Plotly) |
excel |
openpyxl, python-calamine | Read File (Excel), Write Excel |
parquet |
pyarrow, fastparquet | Read File (Parquet), Write Parquet |
polars |
polars, fastexcel | the fast polars engine on Read File |
geo |
geopandas, folium | maps in a web-view node |
ai |
requests | the local-LLM node assistant |
dev |
pytest, pytest-qt | running the test suite |
pip install "flograph[matplotlib,plotly,excel]"
You don't have to decide up front — Tools > Manage Packages installs into the running environment, and a node can use a package the moment it's there.
The project was renamed from flopy to flograph:
flopywas already taken on PyPI (USGS MODFLOW).
One-file script (no install)
If you can't install packages on a given machine — locked-down work laptop,
no index access — but PySide6, pandas, jedi and psutil are already there,
build a single self-contained .py:
python scripts/build_onefile.py # -> dist/flograph_onefile_<version>.py
python flograph_onefile_<version>.py # run it anywhere, no install
It embeds flograph's own source as a base64 zip and unpacks to a temp dir at startup. It does not bundle the third-party dependencies themselves.
Run it
flograph # after install
python -m flograph # equivalent
python main.py project.flograph # open a project from a checkout
python -m flograph.engine.headless project.flograph # run it with no GUI
File > Create Desktop Shortcut… writes a desktop shortcut that starts
flograph the same way it's running right now — a .lnk on Windows, a
.desktop entry on Linux, a .command script on macOS. It names this
environment's interpreter by absolute path (so nothing depends on what
python means to the desktop) and the entry point you're actually on: the
one-file bundle's own .py if that's how you started, otherwise
-m flograph. If a project is open it can be baked into the shortcut, so
double-clicking opens flograph with that flow loaded.
File > Open Example ships twelve worked projects — filter-and-visualise, an aggregate dashboard, a custom-script chart, join/group-by comparison, an interactive slicer dashboard, a scripted pipeline in a frame, a retail ops command centre, two geo/folium maps, an SVG retrofit workbench that diffs a redrawn SVG against the page already wired to it, a Goto/From workflow that runs a three-branch report off one source prep with no wires crossing the page, and a parallel-branches demo whose six independent branches run at once — time it, then set Nodes to run at once to 1 and time it again. They're the fastest way in.
The idea
Nodes are Python scripts. Every node — the shipped library included — is
one small module: a NODE dict declaring typed ports, an optional PARAMS
list that auto-generates its properties form, and a run(ctx, **inputs)
function. Double-click any node to read or fork its code in the built-in
editor, with syntax highlighting, jedi completion, find/replace, and error
markers on the failing line. There is no privileged built-in tier: the
Group By node is a file you can open and change.
Dataflow semantics. Data flows through typed ports; execution is a
topological walk of the dirty subgraph on background threads, so re-runs
only recompute what actually changed. Branches that do not depend on each
other run at the same time — a node starts as soon as its own inputs are
ready, up to a worker limit (Settings > General > Nodes to run at once; Auto
by default). A node that cannot share the process says so with
NODE["exclusive"] = True and runs on its own. Outputs are cached per node.
Status LEDs read at a glance: grey idle, yellow queued, pulsing blue running,
green done, red error — and a node that reports ctx.progress(0..1) from its
loop fills that LED as a ring instead of pulsing, with the percentage beside
the node's name in the status bar. Cancellation is cooperative
(ctx.check_cancelled()) and stops every node in flight.
Inspect everything. Click any node or wire to see the data on it — a paged table view for DataFrames (millions of rows are fine), matplotlib figures with a toolbar, pretty-printed objects. Per-node stdout and tracebacks land in the Log dock, with the traceback mapped back to the line in your node script.
Projects are plain JSON (.flograph) — diffable, reviewable, and small.
Node output caches are written to a side-car <project>.flograph.cache/
directory keyed by a fingerprint of each node's source, params and
everything upstream, so reopening a project restores the results you had
without a re-run. A stale or corrupt entry just leaves that node dirty; it
can never block a load.
Canvas
| Action | Binding |
|---|---|
| Add node | Tab (search palette), right-click, or drag from the library |
| Connect | drag from a port; drop on empty canvas to pick a compatible node |
| Reroute dot | double-click a wire (double-click the dot again to name it) |
| Comment frame | Ctrl+G around the selection (frames move their contents) |
| Run all / selected / cancel | F5 / F6 / Esc |
| Run to this node | right-click a node |
| Pan / zoom | middle-drag or Space+drag / wheel |
| Frame view | F |
| Nudge selection | arrow keys |
| Raise / lower | Ctrl+] / Ctrl+[ — add Shift for front / back |
| Duplicate / delete / rename | Ctrl+D / Del / F2 |
| Settings | Ctrl+, |
| Undo anything | Ctrl+Z — every graph mutation is on the undo stack |
A node that is only a step in the pipeline draws as a fixed 60×60 square — a mark inside, its name floating above, its status light below — so a graph of them reads as a pipeline rather than a row of differently-sized boxes. The size never varies; a node with more ports than fit runs them onto the canvas below. Cards keep their own size.
Right-click → Appearance… is everything about how one node looks, in one live dialog: its shape, its colour, its port names, and its mark — the category default, a different drawn mark, a few characters of your own text, or a picture (PNG, SVG or animated GIF, carried inside the project file so it travels with the flow). Turn the squares off for the whole canvas under Settings → Canvas.
Hold Q over the canvas to float every port's name for as long as you hold it. Double-click a node for its properties (or its code, or a rename — your choice in Settings), and Ctrl+double-click for both in a window of its own that you can leave open beside another node's.
Nodes can be recoloured, aligned and distributed, locked into frames, and
stacked in a deliberate front-to-back order. Goto / From nodes give you a
wire without the wire: name a value at the Goto, pick it up at any number of
Froms, and keep a busy canvas readable. A Variables node does the same
for settings rather than values: name something once (data_dir = C:/data)
and write ${data_dir} into any text box in the Properties panel, or read
ctx.vars in a script. Change it in one place and everything that reads it
re-runs — a reference is a real edge in the graph, so ordering and cache
invalidation work exactly as they would with a wire. Secrets stay out of the
project file: ${env:NAME} reads from a .env file you manage under
Tools ▸ Secrets…, and the project stores only its path.
Sometimes the order matters but no data passes — write the file, then read it back. Every node carries a small flow pin off each of its upper corners; drag one to another node's and you have said "that one first". The dashed arc that results carries nothing, but it is a real dependency: the dependent re-runs when its prerequisite changes, its cached result is invalidated with it, and it is held back if the prerequisite fails or is switched off. A node can wait on as many as you like. The pins stay out of the way until you want them — hidden unless something is wired to them, and brought up by holding the same key that floats the port names, by Settings ▸ Canvas ▸ Show flow pins, or per node from its Appearance dialog. Right-click a dashed arc for What is this?, which explains the whole idea. A minimap (toggleable in Settings) and a status-bar resource monitor — system memory, the open project's cache footprint, the selected node's own — keep an eye on the scale of things.
Large graphs get a GPU-accelerated viewport option and zoom-based level of detail, so cards stop rendering their contents when they're too small to read.
Cards, dashboards and controls
Any node can declare NODE["card"] and become a live card on the canvas —
not a preview pane elsewhere, the node is the chart. Card kinds shipped
today: figure, webview, table_viewer, kpi, grid, slicer,
button, note, control, report, plus the structural reroute, goto,
from and vars.
Click + on the page bar to add a dashboard page. Drag nodes onto it and each becomes a tile — the same widget as the canvas card, resizable and arrangeable, showing STALE when its node is dirty. Tiles maximise to fullscreen; pages can be renamed, recoloured, reordered by dragging, and duplicated.
Input controls are the other half of that: a whole node category that you set rather than compute. Slider, Number, Text, Date, Toggle and Choice each carry a caption you write, are typed properly so wires still validate, and re-run everything downstream when you move them. A Slicer does the same for picking values out of a column. The result is a dashboard you can hand to someone who will never open the model canvas: they turn the knobs, the charts answer.
A control's options and bounds can come from its own optional input ports — wire a column into a Choice node and its dropdown is that column's values.
Report pages
The other page kind is a report: markdown that you write, with your results dropped in by name.
# Q3 review
Revenue came to ![[Total Revenue]] across ![[Region Count]] regions.
![[Revenue by Region]]
![[Sales Table|filtered]]
![[Label]] embeds a node's output — a figure, a table, a scalar, a
markdown string — resolved by node label, with ![[Label|port]] picking a
specific output port. Embeds render inline mid-sentence for scalars and as
blocks for charts and tables, update when the flow re-runs, and warn visibly
when a name doesn't resolve. The page prints to PDF at 300dpi; the
preview and the PDF are literally the same document, so they can't disagree.
There is also a Report card (Viz > Report) — the same markdown, but as
a node inside the flow, embedding its own wired inputs. It edits in place
on the canvas, has a right-click Insert menu listing everything embeddable,
and tiles onto a dashboard. That gives you rich prose on a dashboard, which
a chart tile can't do.
Node library
The library dock shows every node type in its category. Right-click any
node to Add to Favorites (or Ctrl+Shift+F on a selected row) and it
is pinned in a ★ Favorites section at the top; the same star puts
favorites first in the Tab search popup. The star button next to the
search box narrows the whole tree to favorites only. Favorites persist
per-machine in settings.
Input — Slider, Between Slider, Number, Text, Date, Toggle, Choice.
IO — Read File reads CSV, Excel, JSON (incl. JSONL), Parquet and
SQLite through one node: pick the Format (or leave it on auto and let
the extension decide) and the Engine — polars parses in Rust and
releases the GIL, so several readers genuinely run at once. The single-format
Read nodes are all still there, as are Write CSV/Excel/Parquet/JSON/SQLite;
drag a file onto the canvas to get the right reader already configured. Read
CSV/Excel/Parquet (Folder) reads a whole directory as one stacked table, and
Read CSV (Folder → Dict) hands back one table per file. Table is a real spreadsheet you edit on the canvas,
with formulas (=SUM(A1:A9), plus AVERAGE, ROUND, POWER, CONCAT,
LEFT/MID/RIGHT, AND/OR/NOT and the rest of the usual set), fill,
copy/paste, and an optional linked input that keeps its contents when you
disconnect.
Transform — Select Columns, Filter Rows, Sort, Join, Group By, Expression, Concatenate, Missing Values, Duplicate Row Filter, Rename Columns, Pivot, Unpivot, Row Sampling, Convert Types, String Manipulation, Statistics, Data Profile.
Viz — Show Table, Show Plot (matplotlib, live on-canvas), Show Plotly (a real interactive plotly.js chart embedded on the canvas — hover, zoom and pan in place), Show Web View (render anything that produces HTML: folium maps, altair, bokeh, your own template), Card (a Power BI-style KPI number), Table Spec (the incoming table's structure), Chart per Value and Chart per Value (Plotly) — one chart per distinct value of a column, as a stack, in either backend — Slicer, Image (any picture on the canvas, animated GIFs included, from a file or a base64 string), Report.
Any web-view node has Open in Browser on its right-click menu — the same document, in a real browser, refreshed in place when the flow re-runs.
Util — Constant, Reroute, Note, Action Button, Goto, From, Variables.
Scripting — Python Script, plus Node Template and Control Template to fork when you're writing your own.
Writing a node
"""My Node
The first paragraph of the docstring shows in the properties panel.
"""
NODE = {
"label": "My Node",
"category": "Transform",
"inputs": [("table", "dataframe")],
"outputs": [("result", "dataframe")],
}
PARAMS = [
{"name": "factor", "type": "float", "default": 1.0},
]
def run(ctx, table):
ctx.log(f"scaling by {ctx.params['factor']}")
ctx.check_cancelled() # cooperative cancellation
ctx.progress(0.5) # 0..1 through a long loop; throttled
return {"result": table * ctx.params["factor"]}
Port types: any, dataframe, series, number, string, bool, object, figure.
Param types include string, text, int, float, bool, choice, columns, date, password, file_open, file_save, folder_open. A columns param renders with
a ▾ picker listing the columns of the DataFrames cached on the node's inputs
(run upstream once to populate it); add "multi": False so picking replaces
instead of toggling a comma list.
Rules that matter:
- Treat inputs as read-only — outputs are cached by reference, so a write
that escapes your node rewrites what every other branch reads. The engine
guards what it can guard for free: a pandas input arrives as a
copy-on-write shallow copy, and a list, dict, set or bytearray is rebuilt
one level deep, so appending to a list or assigning a column stays local to
your node. A numpy array arrives read-only and raises if you write to it —
arr = arr.copy()first. Reaching through an input (rows[0]["x"] = 1), and anything else you pass between nodes, remain yours to copy. - Heavy imports go inside
run(). Node scripts are executed to be read, so a top-level import runs at library-load time. - matplotlib: the OO API only (
matplotlib.figure.Figure()), neverpyplot— it isn't thread-safe from the worker. - A list output renders as a stack. Return a list of figures and every surface that draws one figure draws them stacked. That's the whole "one chart per value" mechanism — the loop lives in your script, not in a faceting UI.
Add "card": "figure" (or webview, table_viewer, kpi, grid, …) to
give the node a live card. For an input control, "card": "control" plus
"control": "slider" — one host renders every control shape from that and
your PARAMS, so a new control node is usually just a script.
Drop new .py files under src/flograph/nodes/<category>/ (or your user
nodes directory) and they appear in the library on next launch. If a node's
import is missing, it loads as a broken placeholder that keeps its code and
params — install the package, re-apply the code, and it repairs itself.
AI assistant (optional)
Tools > AI Assistant Settings points flograph at any local OpenAI-compatible chat server — Ollama, LM Studio, llama.cpp. You can then describe a change in English ("filter out rows where price is negative") and get a rewritten node script. It is never applied automatically: the reply lands in the editor for you to read, and Apply stays a separate, explicit action. Nothing leaves your machine unless you point it somewhere that isn't local.
Packages
Tools > Manage Packages installs, upgrades and uninstalls pip packages in
flograph's own environment. Nodes execute in-process, so anything installed
there is importable from a node's run() immediately — no restart for new
installs; upgrades of already-imported modules take effect next launch. The
dialog uses pip when the interpreter has it and falls back to uv pip
(uv-made venvs ship without pip). flograph's own core dependencies are
protected from uninstall.
Settings
Ctrl+, opens a searchable two-column settings grid with a navigation tree:
General (window behaviour, resets), Canvas (display, snapping, colour
muting strength, GPU viewport, previews, page-bar position), Table Node,
and About. Selecting a group narrows the grid; the search box filters
across the page.
Development
uv pip install -p .venv/bin/python -e ".[dev]"
QT_QPA_PLATFORM=offscreen .venv/bin/python -m pytest tests/ -q
Architecture (src layout):
flograph/core— Qt-free model: graph, typed ports, script contract, registry, JSON serialization, spreadsheet engine, report parsing, layering. Fully unit-testable; a poison test keeps Qt and pandas out of its import graph.flograph/engine— background execution: plan builder, concurrent dispatch over a thread pool, output cache and its on-disk persistence, cancellation, per-node stdout capture routed by thread, tracebacks mapped to node script lines.flograph/nodes— the standard library; each node is a script file loaded as text through the same contract as user code.flograph/ui— canvas (QGraphicsView from scratch), dashboard and report pages, code editor, inspector, properties, console.
Two invariants hold everywhere:
core/is Qt-free, enforced by a test that imports it in a subprocess and asserts PySide6 and pandas never appear.- QUndoCommands are the sole writers to the graph. UI items react to
graph events; nothing mutates the graph from a click handler. That is why
Ctrl+Zworks on literally everything.
See AGENTS.md for the full contributor briefing.
Changelog
See CHANGELOG.md for what's new in each version.
License
MIT — free for commercial and private use, modification and redistribution; just keep the copyright and license notice.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flograph-0.1.9.tar.gz.
File metadata
- Download URL: flograph-0.1.9.tar.gz
- Upload date:
- Size: 1.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Bazzite","version":"44","id":"Kinoite","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
265591219306125147f6372cbd67ae75880d00530ec0d7f6e6513d10a723e20c
|
|
| MD5 |
3f2cdd74e0de521b54f2b82d195b7cce
|
|
| BLAKE2b-256 |
6e66f6670501a5a4eb62fd53ce8ac972232572f76a58805203b896f7b8f470cd
|
File details
Details for the file flograph-0.1.9-py3-none-any.whl.
File metadata
- Download URL: flograph-0.1.9-py3-none-any.whl
- Upload date:
- Size: 772.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Bazzite","version":"44","id":"Kinoite","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2daf36756e0752494154db97742678332c89b858c8faeb1ad60b35c45ac3e787
|
|
| MD5 |
5befb075635f77e677385beb04557b80
|
|
| BLAKE2b-256 |
76ec6db258e8e3f032307aabd571f0d3613adac931f30afe0effee14ba994085
|