Visualization
GestaltDB ships interactive HTML/JS graph visualization with the core library. The front end (D3.js + React) is prebuilt at library-dev time and packaged with the Python distribution, so rendering artifacts needs no JavaScript toolchain, no network access, and no new runtime dependency.
Quick Start
from tempfile import TemporaryDirectory
from gestaltdb.graphdb import Edge, GraphDB, Node
from gestaltdb.kvstores import LevelDBStore
from gestaltdb.serializers import PickleSerializer
with TemporaryDirectory() as tmpdir:
graph = GraphDB(LevelDBStore(path=f"{tmpdir}/graph"), PickleSerializer())
try:
graph.put_node(Node(node_id="alice", labels=["Person"], properties={"name": "Alice"}))
graph.put_node(Node(node_id="bob", labels=["Person"], properties={"name": "Bob"}))
graph.put_edge(Edge(edge_id="e1", source="alice", target="bob", properties={"type": "knows"}))
figure = graph.visualize('MATCH (a:Person)-[r:knows]->(b) RETURN a, r, b')
figure.save("/tmp/knows.html") # open in any browser, offline
finally:
graph.close()
In Jupyter, the last line can simply be figure: VizFigure renders
inline through _repr_html_ (a height-bounded srcdoc iframe).
Entry Points
from gestaltdb.viz.api import VizOptions, visualize_nodes_edges, visualize_query, visualize_sample
# Explicit collections.
figure = visualize_nodes_edges(nodes, edges)
# Cypher matches (matched entities are highlighted in the canvas).
figure = visualize_query(graph, 'MATCH (a:Person) RETURN a LIMIT 25')
# Typed sampling around seeds (the large-graph path).
from gestaltdb.sampling import SamplingHop, SamplingPattern
pattern = SamplingPattern([SamplingHop("binds", direction="out", sample_size=5)])
figure = visualize_sample(graph, ["drug-1"], pattern)
# ML training batches mapped back to external IDs.
from gestaltdb.viz.api import visualize_sampler_batch
figure = visualize_sampler_batch(batch, snapshot)
Options (VizOptions) cover node/edge caps (defaults 2000/5000),
absolute ceilings (default twice the caps, raising
VizCapExceededError with sampling guidance), iframe height, light/dark
theme, force-layout charge and link distance, artifact title, and
label/property visibility. Any deterministic truncation warns with
TruncationWarning and is bannered in the canvas with exact counts.
Readable Labels
node_label_property selects a property to display instead of the node ID;
missing or null values fall back to the ID. edge_label_property selects an
edge property, falling back to the relationship type (or edge ID for untyped
edges). Reserved metadata such as score is also available as a label source.
label_max_length limits displayed text (default 40 characters).
edge_labels accepts "off", "selected" (the default: selected,
hovered, or keyboard-focused relationships), or "all". The viewer exposes
this setting independently of node-label visibility. Parallel relationships,
reciprocal relationships, and self-loops have distinct directed paths and
independent selection targets.
Direct SVG Output
SVG generation from Python requires no browser, JavaScript runtime, or additional dependency. It uses a deterministic circle or grid layout, or supplied node coordinates. Coordinates must cover every displayed node and are uniformly scaled into the output viewport.
from tempfile import TemporaryDirectory
from pathlib import Path
from gestaltdb.graphdb import Edge, Node
from gestaltdb.viz.api import VizOptions, visualize_nodes_edges
figure = visualize_nodes_edges(
[Node(node_id="alice", properties={"name": "Alice"}),
Node(node_id="bob", properties={"name": "Bob"})],
[Edge(edge_id="e1", source="alice", target="bob",
properties={"type": "knows"})],
options=VizOptions(node_label_property="name", edge_labels="all"),
)
svg = figure.as_svg(width=1200, height=800, layout="circle")
with TemporaryDirectory() as tmpdir:
figure.save_svg(Path(tmpdir) / "graph.svg", layout="grid", background=False)
as_svg() and save_svg() render the Python payload and options, not the
current iframe’s force layout, selection, filters, or manually dragged positions.
For static relationship labels, set edge_labels="all"; static output has no
hover or selection state. positions={"alice": (0, 0), "bob": (1, 0)} can be
passed to either method. save() continues to write interactive HTML.
PNG output is available through the browser’s Export PNG control; direct Python
PNG rasterization is not provided.
Attribute Styling
VizOptions supports node_size_property, node_color_property,
edge_width_property, and edge_color_property in interactive and static
SVG rendering. The browser also exposes these settings under Labels and
attribute styling.
Numeric node radii map to 4–24 and edge widths to 0.5–6, based on the full payload’s finite numeric values. Missing and nonnumeric values (including booleans) use the defaults 7 and 1.2. Constant numeric values map to the midpoint. Color properties are categorical; missing values fall back to the native node group or relationship type. Labels and selection provide additional identity cues when palette colors repeat.
options = VizOptions(
node_label_property="name",
node_size_property="mass",
node_color_property="category",
edge_width_property="score",
edge_color_property="status",
edge_labels="all",
)
Image Export and Saved Views
The Export and saved view panel offers current-viewport or all-visible-content SVG/PNG output, themed or transparent backgrounds, output width/height, and PNG scale 1–4. Width/height must be integers from 1 to 8192; PNG output is limited to 32 megapixels. SVG preserves aspect ratio, so differently shaped output dimensions may introduce padding. Exports preserve active styling and filters.
Save view state downloads a versioned JSON file containing positions, pins, zoom, viewer settings, selection, filters, and active simulation membership. Load view state accepts a local file for the same embedded graph topology and validates all fields before applying changes. No server or notebook kernel communication is required. Loading a paused view restores the arrangement exactly; loading an unpaused view resumes simulation from the saved positions. Download graph JSON remains a separate data export.
To reuse saved coordinates in Python, read the view JSON and pass its positions
to save_svg; this does not apply the saved viewport, selection, or filters:
import json
state = json.loads(Path("gestaltdb-view.json").read_text())
positions = {node["id"]: (node["x"], node["y"])
for node in state["layout"]["nodes"]}
figure.save_svg("arranged.svg", positions=positions)
Canvas Features
D3 force layout with drag-to-pin, double-click release, pan/zoom, refit, pause, and tunable charge/link distance.
Dragging also works while paused; fit-visible includes displayed geometry and labels. The viewer refits when its container is resized.
Explicit pin/unpin actions and dashed pin indicators, full-layout reset, and visible-only relayout. Relayout-visible excludes hidden nodes/relationships from the simulation until another relayout or a saved view is loaded.
Deterministic colorblind-safe coloring by node group and edge type, with a count legend that doubles as a visibility filter.
Click/keyboard selection with a properties inspector, hover tooltips, search by id/label/property, incoming/outgoing/both neighborhood focus with 1–5 hops over enabled relationship types and node filters, and a highlight overlay for Cypher matches (toggleable).
Client-side export: SVG snapshot, PNG raster, and JSON payload download. SVG/PNG capture the selected image scope with resolved theme and label styling. JSON contains graph data, not exploration state; view state is a separate export. Everything runs locally inside the artifact, with export errors shown inline.
A keyboard-friendly visible-entity selector, collapsible inspector, and responsive controls for narrower notebook cells. Native-label filtering includes secondary labels, independently of primary-group filters.
Offline Guarantee
Artifacts are single self-contained .html files: the JS/CSS bundle and
the JSON payload are inlined, there are no CDN or font requests, and the
markup carries an inline-only Content Security Policy. Opening a saved
file over file:// with networking disabled renders fully.
Full runtime dependency license/notice texts are packaged and embedded in each
HTML artifact; Download third-party notices saves them locally.
Large Graphs
Never dump a whole 100k-node database into the DOM: payloads beyond the
absolute ceiling raise an actionable error pointing at
visualize_sample() and LIMIT-bounded visualize_query(). Caps
keep the first IDs in sorted order, so repeated builds are byte-identical
and truncation counts are exact.
Front-End Development
The React + TypeScript + Vite source lives in web/ and builds into
src/gestaltdb/viz/static/ (committed). npm run build reproduces the
bundle byte-identically (modulo the manifest timestamp);
npm run check:licenses gates every dependency against a permissive
allowlist (MIT/ISC/Apache-2.0/BSD/CC0). Python tests never need npm.
The manifest records resolved dependency/toolchain versions, hashes for JS/CSS
and notices, and hashes for source/config/lockfile build inputs. In a checkout,
freshness checks detect changed or added frontend source files; installed wheels
verify packaged assets without requiring a frontend source tree. License checks
retain nested package paths and versions and fail for missing dependencies.
Notices conservatively include the complete runtime dependency set, including
tree-shaken modules. npm run check:bundle rebuilds and compares asset/source
hashes against the previous manifest; the Visualization CI workflow runs this
gate and the offline browser tests.
Browser regressions use Playwright as a development-only dependency. After
npm ci and npx playwright install chromium in web/, run
npm run build and npm test. The tests generate a production HTML artifact
with Python and open it over file:// with networking disabled. Set
VIZ_TEST_PYTHON to the Python executable with GestaltDB’s dependencies
installed, if it is not python3.