Metadata-Version: 2.5
Name: flograph
Version: 0.1.11
Summary: Visual node-based Python programming environment (flow-based dataflow, Blueprint-style canvas)
Project-URL: Homepage, https://github.com/redthista/flograph
Project-URL: Repository, https://github.com/redthista/flograph
Project-URL: Issues, https://github.com/redthista/flograph/issues
Author: redthista
License-Expression: MIT
License-File: LICENSE
Keywords: dataflow,etl,gui,low-code,node-editor,pandas,pyside6,qt,visual-programming
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: X11 Applications :: Qt
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: User Interfaces
Requires-Python: >=3.11
Requires-Dist: jedi>=0.19
Requires-Dist: pandas>=2.0
Requires-Dist: psutil>=5.9
Requires-Dist: pyside6>=6.7
Provides-Extra: ai
Requires-Dist: httpx>=0.27; extra == 'ai'
Requires-Dist: requests; extra == 'ai'
Provides-Extra: dev
Requires-Dist: pytest-qt>=4.4; extra == 'dev'
Requires-Dist: pytest-xdist>=3.6; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: duckdb
Requires-Dist: duckdb>=1.0; extra == 'duckdb'
Provides-Extra: excel
Requires-Dist: openpyxl; extra == 'excel'
Requires-Dist: python-calamine; extra == 'excel'
Provides-Extra: fuzzy
Requires-Dist: rapidfuzz>=3.0; extra == 'fuzzy'
Provides-Extra: geo
Requires-Dist: folium; extra == 'geo'
Requires-Dist: geopandas; extra == 'geo'
Provides-Extra: http
Requires-Dist: httpx>=0.27; extra == 'http'
Provides-Extra: matplotlib
Requires-Dist: matplotlib>=3.8; extra == 'matplotlib'
Provides-Extra: parquet
Requires-Dist: fastparquet; extra == 'parquet'
Requires-Dist: pyarrow; extra == 'parquet'
Provides-Extra: plotly
Requires-Dist: plotly; extra == 'plotly'
Provides-Extra: polars
Requires-Dist: fastexcel; extra == 'polars'
Requires-Dist: polars>=1.0; extra == 'polars'
Provides-Extra: scrape
Requires-Dist: beautifulsoup4; extra == 'scrape'
Requires-Dist: html5lib; extra == 'scrape'
Requires-Dist: lxml; extra == 'scrape'
Provides-Extra: sql
Requires-Dist: sqlalchemy>=2.0; extra == 'sql'
Description-Content-Type: text/markdown

# 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.

![status](https://img.shields.io/badge/status-v0.1-blue)

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):

```bash
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), Plotly Style, Plotly Table, Gantt Chart |
| `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, httpx | LLM Enrich / Classify / Extract, and the node assistant |
| `sql` | sqlalchemy | SQL Query, SQL Write |
| `duckdb` | duckdb | DuckDB SQL |
| `http` | httpx | HTTP Request, REST Paginate |
| `fuzzy` | rapidfuzz | Fuzzy Join |
| `scrape` | lxml, beautifulsoup4, html5lib | Read HTML Tables |
| `dev` | pytest, pytest-qt, pytest-xdist | running the test suite |

```bash
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**: `flopy` was 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`:

```bash
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

```bash
flograph                                # after install
python -m flograph                      # equivalent
python main.py project.flograph         # open a project from a checkout
```

### Headless runs

```bash
flograph run project.flograph                       # run it to completion, no GUI
flograph run project.flograph --var region=North    # override a Variables declaration
python -m flograph run project.flograph             # equivalent
```

`flograph run` exits `0` if every node ran clean and `1` otherwise, so a
scheduler (cron, a Dataiku recipe, CI) can treat a canvas project as a batch
job. `--var name=value` rewrites what the project's Variables node declares,
so one flow runs per region or per date with nothing edited in between;
repeat the flag per variable, and a name the flow does not declare is
refused rather than ignored.

From Python, `flograph.run("project.flograph", variables={"region": "North"})`
does the same and raises `RuntimeError` if a node fails.

Either way the engine runs on a Qt event loop, so PySide6 must be installed
— but no display is needed. Only `flograph.core` is strictly Qt-free.

**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 twenty-one 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, 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 —
and walkthroughs of order edges, flow variables, report pages, the Gantt
node, which draws one project plan four times, adding a single feature per
stage, and running a second flow while one is already running, which joins
the first on the pool, and a WinDirStat-style storage treemap that lists a
folder, splits the paths and charts every file sized by bytes. They're the
fastest way in.

## Documentation

**Help ▸ Documentation** (or **F1**) opens an in-app handbook, laid out like a
docs site — a nav tree on the left, the page on the right — covering getting
started, the canvas, the node library, dashboards and reports, flow
variables, headless runs and the shortcut list. The pages are Markdown under
`src/flograph/docs/`, written GitHub-wiki compatible (`[[Page]]` links, a
`_Sidebar.md` for the tree) so the same files can be published to a hosted
wiki unchanged. This README stays the install-and-overview entry point; the
handbook is the day-to-day one.

---

## 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. The statistics window's run history is saved beside
the cache too, so reopening a project shows its previous runs rather than
starting from nothing.

**A project is one file** (`.flograph`) — a zip bundle holding the graph as
JSON plus every node's output cache, keyed by a fingerprint of each node's
source, params and everything upstream, so reopening a project (or handing it
to someone else) restores the results you had without a re-run. Nothing is
inflated until something asks for it, and the archive is written to a sibling
`.tmp` and swapped in atomically, so a crash mid-write leaves the previous
file intact. Cache blobs are zlib-compressed (a setting turns that off), and
the reader sniffs each blob rather than trusting the manifest, so bundles from
every era keep loading. A stale or corrupt entry just leaves that node dirty;
it can never block a load. Saving shows its progress on the status line while
the cache writes in the background, and both the monitor and the save dialog
speak up when the disk is running low or full.

**File > Export Workflow** writes the graph *alone* to a `.flowf` file — plain
JSON, no cached results, small and diffable — made to be committed to git.

---

## Canvas

| Action | Binding |
| --- | --- |
| Add node | `Tab` or right-click (both open the search palette), or drag from the library |
| Connect | drag from a port; drop on empty canvas to pick a compatible node |
| Splice into a wire | drop a library node onto the wire — it lights green while it will take; `Alt` drops without connecting |
| Replace a node | drop a library node onto it — wires that fit the new node's ports come across |
| Reroute dot | double-click a wire (double-click the dot again to name it) |
| Comment frame | `Ctrl+G` around the selection |
| Move a frame | drag its **title bar** — its contents come with it |
| 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.

Select several nodes, right-click one of them, and the menu is about the
**selection**: run them, freeze, lock, deactivate, *Run only when asked*,
appearance, add them all to a page — one undo step each. The node under the
cursor decides only what the labels *say*, so an entry reading *Unfreeze*
leaves the whole selection thawed rather than swapping each node over.

**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.

A finished page can be **locked** from its tab menu, and locked it *is* the
dashboard: the tiles stop moving and resizing, the arranging chrome goes, and
the page stops behaving like a canvas — no zoom, no wheel, no panning, no
rubber band, no context menu. What stays is everything inside the tiles,
which is the whole point of the distinction: slicers still filter, sliders
still move, spreadsheets still take typing, a PDF still turns its pages, and
any tile still maximises. **Scale to fit the window** sits beside the lock
and is independent of it: the page zooms as the window changes size so the
same tiles stay framed, for when the screen a dashboard is opened on is not
the screen it was built on. Both travel with the project.

**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.

```markdown
# 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
and **Write Text**, which puts a string on disk — an exported chart, a
report, anything a script built;
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. **Read PDF** and
**Read PDF (Folder)** turn documents into a table — one row per page, with the
text, so Filter Rows and Group By work on a stack of invoices the way they
work on a CSV, plus a second output listing each document's metadata and
whether it has a text layer at all. **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, in **any of the 28 chart types Plotly Express draws**: line,
scatter, bar, area, funnel, timeline, histogram, box, violin, strip, ecdf,
density heatmap and contour, pie, funnel area, sunburst, treemap, icicle,
scatter matrix, parallel coordinates and categories, 3D scatter and line,
polar and ternary — with the encodings, facets, trendlines, marginals,
animation frames, bins, palettes and axis settings that go with each, shown
only for the charts that have them), Plotly Style (restyles any Plotly
figure — theme, legend, axis titles and tick formats, gridlines, reference
lines, a note — without touching what it plots), Plotly Table (a table drawn
as a Plotly figure, for when it has to match the charts beside it rather
than be read — Show Table is the one for reading), 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, the Plotly one offering the same 28 chart types as Show
Plotly from the same shared setting list — Gantt Chart (a project plan that **works its own dates
out**: give it durations and a depends-on column and it schedules the plan,
so a task that slips pushes everything after it, with phases, progress,
milestones, dependency arrows and a baseline to measure the slip
against, and a third output that hands you the whole chart as a
standalone HTML page), 
Slicer, Image (any picture on the canvas, animated GIFs included, from a
file or a base64 string), PDF Viewer (a page of a document on the canvas or a
dashboard — rendered at the size it is drawn at, so a 400-page report costs
one page of pixels, with chevrons under the page number to turn it: paging
runs nothing and dirties nothing, on a card or a tile alike), 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

```python
"""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()`), never
  `pyplot` — 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** (execution, saving, 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

```bash
uv pip install -p .venv/bin/python -e ".[dev]"
QT_QPA_PLATFORM=offscreen .venv/bin/python -m pytest tests/ -q

# The full suite is ~6 min serial. Run it in parallel (pytest-xdist, in the
# dev extra) — ~1 min on a 12-core machine:
QT_QPA_PLATFORM=offscreen .venv/bin/python -m pytest tests/ -q -n12 --dist loadfile
```

`--dist loadfile` is required with `-n`: test modules share a module-scoped Qt
fixture and isolate `QSettings` per file, so a module must run whole on one
worker. Past `-n12` it stops scaling — `test_frames.py` is the long pole.

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:

1. **`core/` is Qt-free**, enforced by a test that imports it in a subprocess
   and asserts PySide6 and pandas never appear.
2. **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+Z` works on literally everything.

See [AGENTS.md](https://github.com/redthista/flograph/blob/master/AGENTS.md) for the full contributor briefing.

---

## Changelog

See [CHANGELOG.md](CHANGELOG.md) for what's new in each version.

## License

[MIT](LICENSE) — free for commercial and private use, modification and
redistribution; just keep the copyright and license notice.
