Skip to content

feat: graph statistics panel with density, components, reciprocity, degree histogram#424

Closed
shaunpatterson wants to merge 55 commits into
dgraph-io:mainfrom
shaunpatterson:sp/graph-statistics
Closed

feat: graph statistics panel with density, components, reciprocity, degree histogram#424
shaunpatterson wants to merge 55 commits into
dgraph-io:mainfrom
shaunpatterson:sp/graph-statistics

Conversation

@shaunpatterson

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only panel summarising the currently visible graph: node and edge counts, directed density, undirected connected components, reciprocity (fraction of edges with a reverse), degree statistics (avg/median/min/max), a degree-distribution histogram, and the top hubs by degree and betweenness.

The panel reuses the existing Sigma buildGraph so it sees the same topology the renderer draws. Stats are computed lazily — the graph is only built when the panel is open — and the betweenness ranking is skipped on graphs above the 1500-node threshold annotateMetrics already enforces, so opening the panel never freezes the UI.

Toggle sits in the graph toolbar next to the existing filter and style panels. Adding/removing nodes (e.g. via "Show more nodes") updates the panel automatically through the same graphUpdateHack signal the rest of the graph container already watches.

What it shows

  • Overview: nodes, edges, density, reciprocity
  • Connected components: count + size of the largest
  • Degree: average, median, min / max
  • Degree distribution: 10-bin histogram of the live nodes
  • Top 5 by degree
  • Top 5 by betweenness centrality (skipped on >1500 nodes)

Files

  • client/src/lib/graphStats.js — pure-function stat helpers (summarizeGraph, topByAttribute, plus the per-stat primitives)
  • client/src/lib/graphStats.test.js — 25 unit tests covering empty graphs, isolated components, partial reciprocity, and the even-degree median case
  • client/src/components/GraphStatsPanel.js + .scss — slide-out panel (positioned like GraphFilterPanel / GraphStylePanel), dark-mode aware via the existing [data-theme="dark"] hook
  • client/src/components/GraphContainer.js — toggle button + stats graph memoized on (nodesDataset, edgesDataset, graphUpdateHack, statsPanelOpen)

Inspiration

Universally expected by every mature graph viz (Neo4j Browser, Memgraph Lab, Cytoscape, Gephi, yEd). Complements the existing community coloring, betweenness sizing, and shortest-path features without overlapping them — none of those surface a global overview.

Checklist

  • Tests added (graphStats.test.js — 25 cases)
  • Tests pass (241/241)
  • trunk fmt clean (pre-commit hook verified)

shaunpatterson and others added 30 commits June 12, 2026 09:12
Every component test suite has been failing to run. Four independent
breakages, all in test infrastructure - no production code changes:

- ESM-only packages were never transpiled: the babel config lives in
  package.json (file-relative, like .babelrc), so it is not applied to
  files inside node_modules even when transformIgnorePatterns allows
  them through. Added config/jest/babelTransform.js (explicit presets)
  and allowed react-leaflet/@react-leaflet through the transform.
- Jest 26 cannot resolve node:-prefixed core modules required by newer
  transitive deps (cheerio -> parse5/undici). Added shim files under
  config/jest/nodeShims plus a moduleNameMapper rule.
- enzyme 3 requires cheerio/lib/utils, which no longer exists in
  cheerio 1.x final (the lockfile resolves enzyme's ^1.0.0-rc.3 range
  to 1.1.2). Mapped to its new location (dist/commonjs/utils.js).
- jsdom 16 lacks TextEncoder/TextDecoder, web streams, Blob and
  MessageChannel globals that undici needs. Added a Jest-only
  setupFiles polyfill (kept out of config/polyfills.js, which is also
  a webpack entry).

Also split e2e tests out of the default run: they need puppeteer and a
live Dgraph cluster, so 14 suites always failed in a plain checkout.
'npm test' now runs unit tests only; 'npm run test:e2e' runs the rest.

Result: npm test goes from 16/18 suites failing to 4/4 passing
(13 tests). Production build verified unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds two buttons to the graph toolbar (next to zoom-to-fit):

- PNG: composites every canvas in the graph container onto a single
  white-backed canvas and downloads it via toBlob, so it works with
  the current d3 canvas renderer and any future layered renderer.
- JSON: serializes the GraphParser node/edge Maps to plain JSON.
  Edge endpoints may be uid strings or node objects resolved in place
  by d3-force - both shapes export as uids, keeping the output free
  of circular references.

lib/exportGraph.js is covered by 8 unit tests. Verified in a real
browser against a live Dgraph cluster: both buttons download valid
files (the PNG renders the graph, the JSON carries correct uid
endpoints).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swap the hand-rolled d3-force canvas renderer (~1100 lines of manual
hit-testing, arc math and label culling) for sigma.js v3 + graphology:

- WebGL rendering scales to tens of thousands of nodes (the canvas
  renderer struggles past a few hundred); labels get automatic
  collision/density handling.
- ForceAtlas2 layout runs in a web worker, so the UI thread never
  blocks during layout; it auto-stops after 4s.
- Parallel edges between the same node pair fan out as distinct curves
  (@sigma/edge-curve), preserving the sibling-edge behavior.
- Nodes sized by degree (capped); neighbor highlighting on hover;
  drag-to-pin; double-click to expand/collapse preserved.
- Implements the GraphContainer ref API (searchNode/focusNode/
  zoomToFit) with the same matching semantics as the d3 renderer.
- buildGraph keeps the d3-force contract of resolving edge
  source/target uids to node objects in place - EdgeProperties and
  GraphParser.collapseNode depend on that mutation. Covered by 12
  unit tests.
- sigma is mocked under Jest (jsdom has no WebGL2RenderingContext).
- scripts/graph-smoke.mjs: puppeteer smoke test that seeds a local
  Dgraph, logs in, runs a query through the real UI and asserts the
  WebGL canvases render and search/zoomToFit work.
- Drop the d3 dependency (D3Graph was its only consumer).

Verified: 12/12 buildGraph tests, production build, and the browser
smoke test against Dgraph v25 with ACL (3 nodes / 3 edges rendered
via WebGL, search + zoom-to-fit exercised, zero page errors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dgraph returns a per-phase latency breakdown with every response
(extensions.server_latency: parsing/processing/encoding/...), but
Ratel discarded it - and the frame header latency bar was dead code:
it read frame.serverLatencyNs while the timing lives on
frameResults[id][tab], so it never rendered at all.

- lib/latency.js: pure helpers turning server_latency into ordered,
  labelled bar segments (known phases in pipeline order, unknown *_ns
  fields included future-proof, total_ns excluded) plus tooltip text.
  9 unit tests.
- frames reducer keeps the raw server_latency on the frame result.
- FrameHeader now receives tabResult and renders a multi-segment
  color-coded bar (parsing/processing/encoding/network) with a
  per-phase tooltip showing times and percentages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a Download CSV action to the frame results toolbar for query
frames with response data. Query responses are flattened into rows
(dot-notation for nested objects, '; '-joined scalar arrays,
JSON-stringified object arrays, __block column for multi-block
responses) and serialized as RFC-4180 CSV, downloaded as
ratel-results-<YYYY-MM-DD-HHMM>.csv.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a -url-prefix flag (with RATEL_URL_PREFIX env fallback) to the Go
server so Ratel can be hosted under a subpath behind reverse proxies,
e.g. https://example.com/ratel/ (fixes dgraph-io#390).

When a prefix is set:
- all routes are served under the prefix via http.StripPrefix
- the bare prefix redirects (301) to the prefix with a trailing slash
- root-relative href/src URLs and the inline loader.js reference in the
  served index.html are rewritten to include the prefix
- paths outside the prefix return 404 with a hint at the prefix

With no prefix (the default) behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three graph-view features on top of the sigma renderer:

- Style rules (Neo4j Bloom-style): a Graph styles panel in the toolbar
  lists every group in the current result with a color picker and node
  size slider; overrides apply live via sigma reducers and persist in
  localStorage. lib/graphStyles.js (sanitize/persist/merge) has 11
  unit tests.
- Layout switcher: Force (ForceAtlas2 worker), Circular, and Packed
  (circlepack clustered by group) via a toolbar select; static layouts
  auto-fit the camera.
- Legend filtering: clicking a predicate chip in the entity selector
  hides/shows that predicate's nodes and edges without re-querying;
  hidden chips render dimmed with strikethrough.

Verified in a real browser against Dgraph v25: layouts switch, style
panel renders per-group rows and applies color changes, legend chips
toggle - zero page errors. Unit suite green, production build passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an 'AI' button to the editor toolbar that opens a modal:
describe the query in plain language, get DQL back, review/edit it,
insert into the editor.

- Bring-your-own-key: Anthropic API key entered by the user, stored
  in localStorage only; requests go directly browser -> model API
  (anthropic-dangerous-direct-browser-access) so nothing routes
  through the Ratel server. Model selectable (Haiku default, Sonnet).
- The current schema (schema {}) is summarized compactly (predicates
  with types/indexes, type definitions, dgraph.* filtered out) and
  sent as context, so generated queries use real predicates.
- The system prompt constrains output to a single fenced DQL block;
  extraction falls back to raw text. Generated DQL is editable before
  inserting.
- lib/nl2dql.js is pure and fully unit-tested (12 tests: settings
  persistence/sanitization, schema summarization, prompt content,
  extraction, API call shape, key/error handling).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The form's onSubmit handler called onLogin(userid, password) without
the namespace argument, so keyboard-submitted logins on multi-tenancy
clusters dispatched loginIntoNamespace with Number(undefined) = NaN
instead of the namespace typed into the form. The Login button already
passed it correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Turns Ratel from a read-only viewer into an editor for scalar values
(the headline feature of tools like G.V()):

- Each attribute row in the node properties panel gets edit and
  delete actions: edit opens an inline input (number input for
  numbers, true/false select for booleans), Enter/check saves,
  Escape cancels; delete is two-step (trash -> 'sure?').
- '+ Add value' row sets a new predicate on the node.
- Saves run single-triple N-Quad mutations (commitNow) through the
  existing dgraph client; values are written with the original
  value's type (xs:int / xs:float / xs:boolean, escaped strings) so
  edits never silently retype a predicate. Errors from the server
  surface in the panel; successful edits update the in-memory node.
- lib/mutations.js validates uids (hex) and predicate names (no
  angle brackets/whitespace - no N-Quad injection), escapes string
  literals, and is fully unit-tested (11 tests).

Mutation output verified against a live Dgraph v25: set string with
escaped quotes, set typed int, delete specific/all values - all
accepted and the final state matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generalizes the NL->DQL feature behind a provider registry:

- Anthropic (Claude Haiku 4.5 / Sonnet 4.6), OpenAI (GPT-5 mini /
  GPT-5.1, chat completions with bearer auth) and Google Gemini
  (2.5 Flash / Pro, generateContent with the key in the
  x-goog-api-key header so it never appears in URLs).
- Provider selector in the modal; API keys and model choice are
  stored per provider in localStorage. Legacy single-provider
  settings migrate into the anthropic slot automatically.
- Each provider defines buildRequest/parseResponse/parseError; the
  generation flow, prompt and DQL extraction are shared.
- 22 unit tests: request shapes for all three providers, response
  parsing, settings migration/sanitization, end-to-end mocks,
  error surfacing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a tab strip above the query editor so multiple queries can be
drafted side by side. Tab state lives in the query reducer: the
existing top-level fields remain the live (active-tab) fields, and
switching tabs saves/restores them, so all existing consumers are
untouched. Legacy persisted state without tabs hydrates into a single
tab containing the current query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Editor.scss paints .cm-invalidchar black on purpose: the graphql-ish
editor mode flags valid DQL (dotted predicates like dgraph.type,
numeric arguments) as invalid, and black-on-white makes those tokens
read as normal text. On the dark background they were invisible.
Mirror the intent under [data-theme='dark']: normal text color.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
shaunpatterson and others added 25 commits June 14, 2026 19:52
# Conflicts:
#	client/src/components/EditorPanel.js
# Conflicts:
#	client/src/components/GraphContainer.js
Two UI fixes surfaced when the History button crowds the editor toolbar:

- The toolbar used floats (.actions float left/right). When the right
  action group no longer fit beside Query/Mutate it dropped below,
  collapsing the float container into an empty band + a half-row of
  buttons. Switch .header/.actions to flexbox (flex-wrap, margin-left
  auto) so a crowded toolbar wraps into clean rows.
- The history dropdown was position:absolute right:0 width:420px,
  anchored to the mid-toolbar History button — on a narrow editor pane
  it extended left off-screen and got clipped. Make it position:fixed
  with top/left/width computed from the button rect and clamped to the
  viewport, recomputed on resize, so it always stays visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Louvain community detection and betweenness centrality (graphology
ecosystem) as optional node rendering modes alongside the existing
predicate-color / degree-size defaults:

- Color by: Predicate (default) or Community
- Size by: Degree (default), Centrality (betweenness), or Uniform

Metrics are annotated onto the graphology graph in buildGraph and applied
in the SigmaGraph node reducer, so default rendering is byte-for-byte
unchanged. Betweenness is skipped above 1500 nodes to keep expansion
responsive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a path-finding mode to the graph toolbar. Toggle it on, click a
source node then a target, and the shortest route between them (computed
breadth-first over the currently rendered subgraph, edges treated as
undirected) is highlighted while the rest of the graph dims back. A banner
reports the hop count or that no path exists, with a Clear button.

Path-finding runs client-side on the loaded graph, so it needs no extra
server round-trip and works on exactly what the user can see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a filter panel to the graph toolbar that hides nodes failing a
connectivity (degree) range or an attribute predicate (contains / = / ≠ /
> / < / exists). Edges drop out with either endpoint. The panel previews
how many nodes are hidden and clears in one click.

Filtering is applied in the SigmaGraph reducers via a precomputed
hidden-node set, recomputed only when the filter spec or dataset changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Detect ISO datetime attributes on nodes and, when the dataset spans a
range, offer a timeline control: a clock toggle reveals a scrubber with
play/pause that reveals nodes as their earliest timestamp passes. Nodes
without a time stay as structural context; edges drop with either hidden
endpoint.

The cutoff is applied in the SigmaGraph reducers against a node _time
attribute annotated in buildGraph, so scrubbing/playback is a cheap refresh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	client/src/components/GraphContainer.js
Address review feedback on the latency bar:

- The collapsed headline now shows the grand total (server phases + network),
  not parsing+processing+encoding, matching what users expect from "total".
- Clicking the latency bar opens a 'Query latency breakdown' modal (the hover
  tooltip is kept) with one labelled, colored bar per phase plus a total row.
- A 'Num UIDs' section counts the values each predicate contributed to the
  response (scalars once, child lists by length), a proxy for how much data
  the query returned and thus its processing/encoding/network cost.

countPredicates/numUidSegments are pure and unit-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The EditorTabs strip used hardcoded light colors, leaving a bright bar
above the editor in dark mode. Add [data-theme=dark] overrides so the
strip, inactive tabs, hover and rename input use the dark palette, and
the active tab blends into the editor header (--bg-raised) below it.

(EditorTabs ships on the multi-tab branch; these rules are dormant until
both features are present, e.g. on sp/all-features.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review, the Latency section now reads as a sequence over the total
query duration: each phase bar starts where the previous phase ended
(offset by cumulative elapsed time) instead of every bar starting at
zero. Num UIDs bars stay left-aligned as plain counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The modal chrome is themed centrally, but the latency/num-uids bar tracks
rendered bright white and labels were dim in dark mode. Add
[data-theme=dark] overrides (co-located with the component) so the empty
tracks use the dark elevated surface, the total rule/border use the dark
border, and labels/values stay legible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 120px label column truncated 'Assign timestamp' to 'Assign timesta...'.
Widen to 150px so the longest phase label fits without ellipsis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
variant='default' buttons (the 'Connected' status and 'Return to Ratel')
rendered near-black on the dark modal surface. Give .btn-default the dark
theme text color so they're readable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- GraphContainer: add type="button", aria-label, and title-bearing SVG icons
  to every toolbar button so the controls don't default to submit, expose
  themselves to screen readers, and avoid the noSvgWithoutTitle lint
- EditorTabs: replace autoFocus on the rename input with a ref-based
  focus effect (also selects the existing name so renaming is faster)
- SigmaGraph: drop the ambiguous `(this.draggedNode = null)` arrow body
  that read as a `return null`; use a block body so the assignment is the
  obvious side effect
…egree histogram

Adds a read-only panel summarising the currently visible graph: node
and edge counts, directed density, undirected connected components,
reciprocity (fraction of edges with a reverse), degree statistics, a
degree-distribution histogram, and the top hubs by degree and betweenness.

The panel reuses the existing Sigma `buildGraph` so it sees the same
graph topology the renderer draws. Stats are computed lazily — the
graph is only built when the panel is open — and the betweenness ranking
is skipped on graphs above the 1500-node threshold that
`annotateMetrics` already enforces, so opening the panel never freezes
the UI.

Toggle sits in the graph toolbar next to the existing filter and style
panels. The summary, the two top-N rankings, and the histogram all share
the live dataset, so adding/removing nodes (e.g. via "Show more nodes")
updates the panel automatically through the same `graphUpdateHack`
signal the rest of the graph container already watches.

`client/src/lib/graphStats.js` ships the stat helpers as a small,
standalone module with 25 unit tests covering empty graphs, isolated
components, partial reciprocity, and an even-degree median case.
@shaunpatterson shaunpatterson requested a review from a team as a code owner June 20, 2026 00:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant