Graph viewer

cvcdocdb.viewer is a read-only, interactive viewer for the data in any backend (NetworkXGraph, Neo4jGraph, MemgraphGraph, JenaGraph): node labels with counts, relation types, search by label, property filters and free text with paging, node properties, and a graph canvas where you expand a node’s neighbours. It never writes to the graph.

It runs in two ways: inside a Jupyter notebook, or as a web page served from your machine.

In a Jupyter notebook

Install the notebook extra, which adds anywidget:

pip install "cvcdocdb[notebook]"

and make show() the last expression of a cell:

from cvcdocdb.viewer import show

viewer = show(graph)             # explore from the table and the search
viewer = show(graph, load_all=True)   # small graphs: draw every node and relation
viewer = show(graph, language="ca")   # interface in Catalan ("es": Spanish)

It is an anywidget widget, so it can run in JupyterLab, Jupyter Notebook 7, VS Code and Colab (so far it has been tested in JupyterLab), and it follows the notebook’s light or dark theme. No server is involved: the viewer asks the Python kernel for each piece of data, so the graph is live but needs a running kernel (a saved notebook reopened without one shows no data).

The viewer reads the graph when you ask it for something. After changing the graph from Python, call refresh() to update every view of it:

graph.insertNode(Node(pk={"doc": "DOC-002"}, main_label="Document"))
viewer.refresh()

Closing the graph empties it, and the viewer with it, so close it only when you are done. WeakNode and Nested WeakNode (Interactive) combines the viewer with an ipywidgets control panel that edits the graph.

In the browser

The viewer is also a small web server that needs nothing beyond the standard library:

python -m cvcdocdb.viewer path/to/graph.pkl                     # a NetworkXGraph pickle
python -m cvcdocdb.viewer --backend neo4j --url bolt://localhost:7687 --user neo4j
python -m cvcdocdb.viewer --backend jena --url http://localhost:3030/ds

The remote password is read from CVCDOCDB_PASSWORD (or asked for). The server binds to 127.0.0.1:8765 by default (--host, --port); use --token (or CVCDOCDB_VIEWER_TOKEN) to require a token on the API, and --no-browser not to open the page, and --language ca (or es) for the interface texts in Catalan or Spanish (the data isn’t translated). From Python:

from cvcdocdb.viewer import serve_viewer

serve_viewer(graph)   # http://127.0.0.1:8765/, until Ctrl+C

Security

  • The viewer only reads, through the store’s public API, and it validates and caps every input. The server accepts GET requests only.

  • The server binds to a loopback address by default and then only answers requests addressed to a loopback name. Binding another address exposes the data to the network: set a token.

  • In a notebook, the page runs in a sandboxed iframe with no network access and no access to the notebook page.

  • The page uses a bundled copy of cytoscape.js (MIT), so it makes no requests to other sites.

API

cvcdocdb.viewer.show(store, height=650, load_all=False, language='en')[source]

The viewer for store as a Jupyter widget; make it a cell’s last expression to display it.

Parameters:
  • store (Any) – The graph store to browse, or a configured GraphViewerData (to change its limits).

  • height (int) – Height of the viewer in pixels.

  • load_all (bool) – Show the whole graph on the canvas (up to 300 nodes), for small graphs such as a tutorial’s.

  • language (str) – Interface language: "en" (default), "ca" or "es".

Return type:

Any

Call refresh() on the returned widget after changing the graph.

Returns:

A cvcdocdb.viewer.notebook.GraphViewerWidget.

Raises:

ImportError – If anywidget isn’t installed (pip install 'cvcdocdb[notebook]').

Parameters:
Return type:

Any

The graph viewer as a Jupyter widget (needs pip install cvcdocdb[notebook]).

from cvcdocdb.viewer import show

show(graph)            # last expression of a cell

No HTTP server is involved, so it can run wherever anywidget does (JupyterLab, Notebook 7, VS Code, Colab; tested in JupyterLab). The viewer page runs in a sandboxed iframe that has no network access; it asks the widget for data with postMessage, the widget forwards the request to the kernel, and the kernel answers it with cvcdocdb.viewer.api.handle_api() — the same read-only API the HTTP server exposes. The graph is live: it needs a running kernel, and a saved notebook reopened without one shows no data.

class cvcdocdb.viewer.notebook.GraphViewerWidget(*args, **kwargs)[source]

Read-only graph viewer for store, displayed in a notebook cell.

Parameters:
  • store (Union[GraphStore, GraphViewerData]) – The graph store to browse, or a configured GraphViewerData (to change its limits).

  • height (int) – Height of the viewer in pixels.

  • load_all (bool) – Show the whole graph on the canvas (up to 300 nodes) and keep it whole on refresh() — for small graphs, such as a tutorial’s.

  • language (str) – Interface language: "en" (default), "ca" or "es".

  • kwargs (Any)

The viewer reads the store when the user asks for something. After changing the graph from Python, call refresh() to update it.

height
load_all
__init__(store, height=650, load_all=False, language='en', **kwargs)[source]
Parameters:
Return type:

None

refresh()[source]

Read the store again and update every view of the widget: the sidebar, the table and the canvas (deleted nodes go away; expanded nodes, or the whole graph with load_all, are fetched again).

Return type:

None

cvcdocdb.viewer.notebook.build_page(layout='classic', load_all=False, accent=None, extra_css=None, language='en')[source]

The viewer page with its styles and scripts inlined, set to ask its parent window for data instead of the HTTP API.

Parameters:
  • layout (str) – "classic" or "light" (see the module docs).

  • load_all (bool) – Draw the whole graph on start (up to 300 nodes).

  • accent (str | None) – Accent colour, #RGB or #RRGGBB.

  • extra_css (str | None) – A stylesheet added after the viewer’s own (host look).

  • language (str) – Interface language, one of LANGUAGES.

Raises:

ValueError – On an unknown layout or language, an invalid accent or extra_css.

Return type:

str

HTTP server of the graph viewer (standard library only).

Routes (GET only — anything else gets 405):

  • / — the viewer page; /static/<file> — its scripts and styles.

  • /api/summary — GraphViewerData.summary().

  • /api/nodes?label=&q=&f=prop:op:value&offset=&limit= — GraphViewerData.search() (f may be repeated; the value is everything after the second :).

  • /api/nodes/<id> — GraphViewerData.node().

  • /api/nodes/<id>/neighbours?direction=&type=&limit= — GraphViewerData.neighbours().

  • /api/relations?ids=<id>&ids=<id>... — GraphViewerData.relations_among().

When a token is configured, every /api request must carry it as ?token= or Authorization: Bearer <token> (401 otherwise). The page and its static files hold no data and are served without it; the page forwards the token from its own URL to the API.

The server binds to 127.0.0.1 by default. While bound to a loopback address it only answers requests whose Host is a loopback name, which blocks DNS-rebinding attacks from web pages open in the same browser.

class cvcdocdb.viewer.server.ViewerServer(data, host, port, token, layout='classic', load_all=False, accent=None, extra_css=None, language='en')[source]

The viewer’s HTTP server; url is the address to open.

Parameters:
daemon_threads = True
__init__(data, host, port, token, layout='classic', load_all=False, accent=None, extra_css=None, language='en')[source]

Constructor. May be extended, do not override.

Parameters:
Return type:

None

cvcdocdb.viewer.server.create_server(store, host='127.0.0.1', port=8765, token=None, layout='classic', load_all=False, accent=None, extra_css=None, language='en')[source]

Build (but don’t start) the viewer server for store.

Parameters:
  • store (GraphStore | GraphViewerData) – The graph store to browse, or a configured GraphViewerData (to change its limits).

  • host (str) – Address to bind; 127.0.0.1 by default. Binding anything else exposes the data to the network — set a token.

  • port (int) – TCP port; 0 picks a free one.

  • token (str | None) – If set, required by every /api request.

  • layout (str) – "classic" or "light" (graph first; see cvcdocdb.viewer.page).

  • load_all (bool) – Draw the whole graph on start (up to 300 nodes).

  • accent (str | None) – Accent colour of the page, #RGB or #RRGGBB.

  • extra_css (str | None) – A stylesheet added after the viewer’s own (your look).

  • language (str) – Interface language: "en", "ca" or "es".

Returns:

A ViewerServer; call serve_forever() to run it.

Return type:

ViewerServer

cvcdocdb.viewer.server.serve_viewer(store, host='127.0.0.1', port=8765, token=None, open_browser=False, layout='classic', load_all=False, accent=None, extra_css=None, language='en')[source]

Serve the read-only viewer for store until interrupted (Ctrl+C).

Parameters:
Return type:

None

The store is not closed on exit: it belongs to the caller.

Read-only, backend-agnostic data layer of the graph viewer.

GraphViewerData answers the questions the viewer’s page asks — which labels and relation types exist, which nodes match some filters, what a node holds and what it is connected to — using only the public GraphStore API:

  • dict filters through GraphStore.query() (main_label, $in, $ne, $gt…, $exists, $regex), which every backend translates itself (bound parameters on Neo4j/Memgraph, Python matching on NetworkX/Jena);

  • a few fixed read-only Cypher statements with $parameters for counts, node detail and neighbours (Neo4j/Memgraph run them natively; NetworkXGraph and JenaGraph run them with cvcdocdb.nx_cypher).

User input never becomes query text: labels, property names and relation types are checked against an identifier pattern and passed as filter keys or parameters, values are always bound, and “contains” uses an escaped pattern. Every result is capped (page size, scan size, neighbours), so a large remote graph can’t be pulled whole by one request.

Calls are serialised with a lock: the server is multi-threaded and a Neo4j session isn’t thread-safe.

class cvcdocdb.viewer.data.GraphViewerData(store, max_page_size=200, max_scan=5000, max_neighbours=500, max_relations=2000)[source]

Read-only browsing operations over any GraphStore.

Parameters:
  • store (GraphStore) – An open graph store (NetworkXGraph, Neo4jGraph, MemgraphGraph, JenaGraph…). It is only read.

  • max_page_size (int) – Most results search() returns per page.

  • max_scan (int) – Most nodes one search() reads from the store; the total it reports is a lower bound past this.

  • max_neighbours (int) – Most relations neighbours() returns.

  • max_relations (int) – Most relations relations_among() returns.

__init__(store, max_page_size=200, max_scan=5000, max_neighbours=500, max_relations=2000)[source]
Parameters:
Return type:

None

summary()[source]

Node counts per main label and relation counts per type.

Returns:

A dict with backend, labels ([{"label", "count"}]), relation_types ([{"type", "count"}]), node_count and relation_count; labels and types sorted by name.

Return type:

Dict[str, Any]

search(label=None, filters=(), text=None, offset=0, limit=50, labels=None)[source]

Find nodes by main label, property filters and free text.

Parameters:
  • label (str | None) – Main label to restrict to.

  • filters (Sequence[Tuple[str, str, Any]]) – (property, operator, value) triples, all of which must hold. Operators: eq, ne (numbers/booleans in value also match typed values), contains (case-insensitive substring), gt/gte/lt/lte, exists (value ignored).

  • text (str | None) – Case-insensitive text looked for in every string property.

  • offset (int) – Results to skip (paging).

  • limit (int) – Results to return; capped at max_page_size.

  • labels (Sequence[str] | None) – Main labels to restrict to (any of them; at most MAX_LABELS), together with label if both are given.

Returns:

A dict with items (node summaries), total, total_is_exact, offset and limit. At most max_scan nodes are read, so past that total is a lower bound and total_is_exact is False.

Raises:

InvalidRequest – On an invalid label, property, operator or range.

Return type:

Dict[str, Any]

node(node_id)[source]

All the labels and properties of one node.

Raises:
Parameters:

node_id (int)

Return type:

Dict[str, Any]

neighbours(node_id, direction='both', relation_type=None, limit=50)[source]

Relations of one node and the nodes at their other end.

Parameters:
  • node_id (int) – The node.

  • direction (str) – "out", "in" or "both".

  • relation_type (str | None) – Only relations of this type.

  • limit (int) – Most relations to return; capped at max_neighbours.

Returns:

A dict with node (its summary), relations (each with type, direction, source, target, properties and neighbour, the other node’s summary) and truncated.

Raises:
Return type:

Dict[str, Any]

relations_among(node_ids)[source]

The relations whose two ends are both in node_ids, in one query.

Lets a client draw the edges of a whole canvas without asking for the neighbours of every node.

Parameters:

node_ids (Sequence[int]) – Node ids (at most MAX_RELATION_IDS); unknown ids are ignored.

Returns:

A dict with relations (each with type, source, target and properties) and truncated, true when there were more than max_relations.

Raises:

InvalidRequest – On a non-integer id or too many ids.

Return type:

Dict[str, Any]

exception cvcdocdb.viewer.data.InvalidRequest[source]

A viewer request had an invalid or out-of-range parameter.

exception cvcdocdb.viewer.data.NodeNotFound[source]

No node has the requested id.

cvcdocdb.viewer.data.FILTER_OPERATORS = ('eq', 'ne', 'contains', 'gt', 'gte', 'lt', 'lte', 'exists')

Filter operators accepted by GraphViewerData.search().