Graph Analytics — Architecture¶
Overview¶
Graph analytics runs as a server-side Python pipeline. REQ-642 The browser submits a Cypher query and an algorithm name; the backend executes the query, builds an in-memory graph, runs the algorithm via NetworkX or igraph, and returns augmented node/edge data with algorithm output merged into properties. REQ-642
Endpoint¶
POST /data/graph-analytics
Content-Type: application/json
{
"query": "MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 500",
"algorithm": "pagerank",
"params": { "alpha": 0.85 }
}
Response:
{
"nodes": [
{ "id": "42", "label": "Person", "properties": { "name": "Alice" }, "_analytics": { "score": 0.031, "cluster": 2 } }
],
"edges": [...],
"algorithm": "pagerank",
"elapsed_ms": 142
}
The _analytics object is merged into each node/edge. REQ-643 Keys vary by algorithm (see table below).
Algorithms¶
| Name | algorithm key |
Output keys | Library |
|---|---|---|---|
| PageRank | pagerank |
score |
NetworkX |
| Betweenness centrality | betweenness |
score |
NetworkX |
| Closeness centrality | closeness |
score |
NetworkX |
| Degree centrality | degree |
score, in_degree, out_degree |
NetworkX |
| Eigenvector centrality | eigenvector |
score |
NetworkX |
| Louvain community detection | louvain |
cluster |
python-louvain |
| Leiden community detection | leiden |
cluster |
leidenalg |
| Girvan-Newman communities | girvan_newman |
cluster |
NetworkX |
| K-core decomposition | kcore |
core_number |
NetworkX |
| Local clustering coefficient | clustering |
score |
NetworkX |
(REQ-643)
Backend Implementation¶
Location¶
provisa/api/rest/graph_analytics_router.py
Pipeline¶
POST /data/graph-analytics
│
├─ 1. Execute Cypher query → rows via existing cypher_router pipeline
│
├─ 2. Extract nodes + edges from rows (reuse extractElements logic)
│
├─ 3. Build networkx.DiGraph
│ nodes: id → {label, properties}
│ edges: (src_id, tgt_id, {type, properties})
│
├─ 4. Run algorithm (dispatch table keyed on algorithm name)
│
├─ 5. Merge _analytics dict into each node/edge
│
└─ 6. Return augmented nodes + edges as JSON
Dependencies¶
Add to pyproject.toml optional group [graph]:
- networkx>=3.0
- python-louvain>=0.16 (for Louvain)
- leidenalg>=0.10 (for Leiden, requires igraph)
Algorithm dispatch (pseudocode)¶
ALGORITHMS = {
"pagerank": lambda G, p: nx.pagerank(G, **p),
"betweenness": lambda G, p: nx.betweenness_centrality(G, **p),
"closeness": lambda G, p: nx.closeness_centrality(G, **p),
"degree": lambda G, p: {n: {"score": d, "in_degree": G.in_degree(n), "out_degree": G.out_degree(n)} for n, d in nx.degree_centrality(G).items()},
"eigenvector": lambda G, p: nx.eigenvector_centrality_numpy(G, **p),
"louvain": lambda G, p: community.best_partition(G.to_undirected(), **p),
"kcore": lambda G, p: nx.core_number(G),
"clustering": lambda G, p: nx.clustering(G.to_undirected(), **p),
}
UI Integration¶
GraphFrame changes¶
- "Analyze" button (⬡▸) in frame header, visible when graph data is present
- Opens an
AnalyticsPaneloverlay (dropdown of algorithms + param inputs + Run button) - On submit: POST to
/data/graph-analyticswith current frame's query + chosen algorithm - On response: merge
_analyticsinto frame nodes/edges; re-render graph
Visual encoding¶
Node size and color driven by analytics output:
| Output key | Visual mapping |
|---|---|
score (centrality) |
Node radius scales linearly with score |
cluster (community) |
Node color overridden by cluster ID → PALETTE index |
core_number |
Node opacity by k-core tier |
Cytoscape style functions read ele.data("_analytics") at render time.
State¶
colorOverrides in GraphFrame is extended to support cluster-based coloring:
analyticsOverrides: Record<string, { color?: string; size?: number }>
// keyed by node id, merged into cytoscape style functions
Node Grouping¶
Core Principle¶
Grouping is a view transform, not a data transform. REQ-644 The underlying nodes and edges are unchanged; the rendering layer applies a grouping function node → groupKey derived from any attribute. REQ-644 This keeps the model clean and composable across arbitrary attributes — data properties, injected metadata (domain), or analytics output (cluster).
Attribute Discovery¶
After any query result or analytics pass, scan all node properties to build a per-label map of groupable attributes (categorical fields: strings, low-cardinality integers). REQ-645 domain is always available as a grouping attribute (derived from the node label prefix set by the semantic layer). REQ-645 cluster appears after community detection analytics. REQ-645 Schema cluster attributes (schema_L1, schema_L2, schema_L3) are available when schema clustering has run. (REQ-645) All other attributes come from the data itself.
// Derived from frame nodes after each result
type GroupableAttributes = Record<string, string[]>
// { "Person": ["domain", "industry", "cluster", "country"],
// "Company": ["domain", "sector", "cluster", "region"] }
The UI builds the grouping controls dynamically from this map — no hardcoding of attribute names. REQ-645
Grouping State¶
type GroupingEncoding = "color" | "hull" | "ring";
interface GroupingLayer {
attribute: string; // e.g. "domain", "cluster", "country"
encoding: GroupingEncoding;
colorMap: Record<string, string>; // groupValue → hex color
}
interface GroupingState {
layers: GroupingLayer[];
collapsed: Set<string>; // groupKeys currently collapsed to supernodes
}
Multiple layers are supported simultaneously, each using a different visual channel (color, hull, ring) to avoid overloading any single encoding.
Rendering Phases¶
Phase 1 — Color encoding + convex hull overlays¶
One active grouping attribute at a time. The attribute drives two simultaneous encodings:
- Color: node
background-coloroverridden bycolorMap[node.properties[attribute]]REQ-646 - Hull: translucent filled SVG ellipse drawn around each group's node positions, labeled with the group value REQ-647
Hulls render in an SVG layer overlaid on the Cytoscape canvas. REQ-647 Node positions are read from cy.nodes().positions() after layout stabilizes. REQ-647 Hulls recompute on pan/zoom via a Cytoscape viewport event listener. REQ-647
Phase 2 — Multiple simultaneous groupings¶
Each layer in GroupingState.layers uses a distinct visual channel:
| Encoding | Implementation |
|---|---|
color |
Overrides node background-color in Cytoscape style function |
hull |
SVG ellipse per group value, drawn on overlay layer |
ring |
Outer border-color + thicker border-width on node circle |
Only one layer may use color; only one may use ring. Multiple hull layers are permitted (nested/overlapping hulls with different stroke colors).
Phase 3 — Collapse groups¶
A "collapse" toggle per group in the legend. When collapsed: REQ-648
- All nodes in the group are hidden from view in Cytoscape REQ-648
- A synthetic supernode is added representing the group (count badge, group label) REQ-648
- Edges to/from hidden members are rewritten to point to/from the supernode REQ-648
- Clicking the supernode expands the group and restores original nodes/edges REQ-648
Supernode ids use a reserved prefix to avoid collision with real node ids. REQ-648
UI Control Placement¶
Grouping controls belong in the frame header actions bar inside gf-header. The Inspector (right panel) is for selected-item details only — grouping is a frame-level concern.
┌─────────────────────────────────────────────────────────┐
│ [header actions] … ⬡ group [domain ▾] │ ← gf-header-actions
├─────────────────────────────────────────────────────────┤
│ │
│ Cytoscape canvas │
│ │
└─────────────────────────────────────────────────────────┘
Header actions components:
gf-attr-selectdropdown: populates from discovered groupable attributes for the current frameGroupLegend: shows each group value as a colored swatch; each swatch has a collapse toggle (Phase 3)
Inspector read-only display: The Inspector panel shows the selected node's group membership as additional read-only fields alongside its other properties (e.g. domain: Finance, cluster: 3). No controls here.
Component Structure¶
GraphFrame
├── gf-header (query, view buttons, gf-attr-select grouping dropdown)
└── gf-graph-area
├── GraphCanvas (Cytoscape)
├── HullOverlay (SVG) ← hull ellipses
└── Inspector
GroupingState is held in GraphFrame state and passed down to both GraphCanvas (for color encoding) and HullOverlay (for SVG hull drawing). REQ-644 gf-attr-select receives the groupableAttributes map derived from the current frame's nodes. REQ-645
Interaction with Analytics¶
Analytics output (_analytics.cluster, _analytics.score) is just more node data. The grouping system treats it identically to any other attribute: REQ-644
- After a community detection run,
clusterappears ingroupableAttributes(REQ-643) - The user can immediately select "Group by cluster" in the controls bar
- Nodes are colored and hulled by cluster ID automatically REQ-646
- Centrality
scoreis not groupable (continuous, not categorical) — it drives size encoding instead REQ-649
Constraints¶
- Max graph size for analytics: 10,000 nodes / 50,000 edges (configurable). REQ-650 Return 413 if exceeded. REQ-650
- Algorithms run synchronously in the request thread for now. Move to background task if p99 > 5s.
- Leiden requires
igraphC extension — document build dependency inDockerfile. - Girvan-Newman is O(n³); restrict to graphs < 500 nodes or require explicit
force=trueparam. REQ-651