The viewer

kglite-visual graph.kgl opens one page: a WebGL canvas on the left and a column of panels on the right. This page is the tour. Four subjects are big enough to have their own:

  • The honesty model — bounds, truncation, and every place the app tells you what it is not showing. This is the product philosophy, not a feature list.

  • Layouts — the live GPU force simulation, the static kernels the server computes, and the geographic map.

  • Query surfaces — the Cypher editor, saved queries, the generated table, the path builder, PROFILE and EXPLAIN.

  • Appearance — colour, size, captions, the legend and the client-side filter.

The entry screen is the meta-graph

A .kgl file can hold more nodes than any browser will render, so the first view is never the whole graph. It is the type-level meta-graph: one node per node label, one link per relationship type, each carrying its count. That picture stays small whatever the graph underneath — 98 nodes for a graph of 546,850 — and it is the only entry screen that works at every scale kglite supports.

The encoding is the same one the renderer uses, so an exported image and the app agree:

Channel

Meaning

Circle area

Member count, log-scaled

Link width

Number of edges the relationship type stands for

Muted styling

A supporting type — one that hangs off another in the graph’s own type hierarchy

GEO / LOC / TS / VEC badge

The type declares a WKT geometry / a lat/lon pair / a timeseries / embedding vectors. GEO and LOC are independent, so a type declaring both shows both

Detail tiers

The server asks kglite for a schema sized for the graph it has, and reports which tier it used on stderr and in GET /api/session:

{"protocol_version":4,"tier":"compact","slot_count":98,
 "stats":{"node_count":546850,"edge_count":765373,"node_type_count":98,
          "relationship_type_count":54,"core_type_count":35}}

core_type_count is the number of types the graph is about, as opposed to the supporting types hanging off them — 35 of 98 in the example. The tier decides how much per-type detail the schema carries, not what the picture draws.

Selection, preview, expansion

Clicking a node does not load anything. It fills the Selection panel with what expanding it would add — every relationship that type actually has, in each direction, with a count:

$ curl -s -XPOST $B/api/preview -H 'content-type: application/json' -d '{"slot":0}'
{"protocol_version":4,"slot":0,"scope":"type","node_type":"Person","title":"",
 "relationships":[
   {"name":"HAS_SKILL","direction":"out","other_type":"Skill","count":180},
   {"name":"KNOWS","direction":"out","other_type":"Person","count":180},
   {"name":"KNOWS","direction":"in","other_type":"Person","count":180},
   {"name":"CONTRIBUTES_TO","direction":"out","other_type":"Project","count":93},
   {"name":"WORKS_AT","direction":"out","other_type":"Company","count":60}],
 "total_edges":693,"max_nodes":5000}

Preview before expansion is the whole progressive-disclosure idea in one interaction: you learn the size of the answer before it costs anything.

Then expand. A type slot loads instances of that type; an instance slot loads what it is connected to. Naming a relationship is the cheap case; walking every relationship a type has is the expensive one. max nodes in the panel is a request — the ceiling is enforced in core, and the answer reports what it cut.

Collapse removes an expansion again. Collapsing a selection also clears it, so the counts the viewer reports never describe nodes that have left the screen. Slot numbers are not reissued, so anything holding a slot stays valid unless the answer carries a compaction — which renumbers everything, and says so.

Inspecting a node

Selecting an instance node fills the panel with its properties, and the type panel offers property statistics for the type as a whole: per property, the value type, how many nodes carry it, how many distinct values there are, and either the full value set or a sample.

{"node_type":"Person","node_count":60,"sampled":false,"exact_scan_ceiling":200000,
 "properties":[
   {"name":"city","value_type":"String","non_null":60,"unique":10,
    "values":["City_0","City_1","…"],"approx":false,"role":"categorical"},
   {"name":"age","value_type":"Int64","non_null":60,"unique":33,
    "values":[],"sample":45,"approx":true,"role":"numeric"}]}

approx and sampled are not decoration. Above the exact-scan ceiling the statistics are estimated, and the app says which numbers are estimates — the same rule as everywhere else in this program.

Those statistics are also what drives the appearance pickers: a property with ten distinct values over full coverage is a good colour channel, and one with sixty unique strings is not.

Read-only, always

The viewer runs queries read-only. A statement that writes is refused before it runs — POST /api/validate reports it as an error with the sentence “this viewer runs queries read-only — the engine will refuse a statement that writes”, and the editor shows it under the query box while you are still typing.