Inside TypeDB Studio: Part 3 - Exploration

Alex Walker


TypeDB Studio is the official interactive environment for TypeDB – a database that reimagines data modelling around entities, relations, and attributes rather than tables or documents. Studio gives developers a visual, multi-modal interface for building TypeQL queries, visualising schema and exploring data instances, all from the browser or as a native desktop app.

This is the final post in the TypeDB Studio internals series, following on from Part 2: Querying, and dives into the implementations of the Data Explorer and into graph visualisation of TypeDB query responses. This post would be of interest to enthusiast developers building their own TypeDB applications or tooling, and to learners wishing to study TypeQL functionality.

TypeDB Studio is fully open source, and you can browse its source code at https://github.com/typedb/typedb-studio.

Data Explorer

It is a fundamental user expectation that every database be easy to visualise as a table. We have grown so accustomed to seeing data in tables that it feels natural regardless of the storage format – be it graph, document or relational.

TypeDB Studio’s data explorer gives users a tabular interface for browsing the instances in a database – entities, relations, and attributes – with pagination, filtering, sorting, and drill-down into individual instances. It is the most query-intensive part of Studio, generating TypeQL dynamically behind the scenes so the user never has to.

A table is a very flat structure by nature, in stark contrast to the rich interconnected data model of TypeDB. Figuring out what attributes are available is simple enough: walk ownedAttributes recursively through subtypes of the visualised type, and collect every attribute type that any instance of the target type could own. But fetching a paginated, sortable, filterable table of instances with their attributes requires some stitching in TypeQL. One might attempt to use the following simple query, but it doesn’t quite work. Can you spot the two issues?

# does not work in this case
match
    $instance isa person;
    $instance has name $name;
    $instance has age $age;
    $instance has email $email;
select $instance, $name, $age, $email;

Firstly, the attributes may be optional. The above query doesn’t just return NULLs if attributes are missing – it will simply not return results. It requires all attributes to be present in order to return anything at all.

Secondly, naively joining instances with all their attributes would produce a cartesian product across multi-valued attributes, breaking pagination.

Studio solves these two issues with a query pipeline composed of two match statements, where the second one uses try to optionally retrieve attributes:

match
    $instance isa person;
offset 0; limit 100;

match
    try { $instance has name $name; };
    try { $instance has age $age; };
    try { $instance has email $email; };
select
    $instance, $name, $age, $email;

try has another advantage here: it allows for the elegant handling of type hierarchies. Say you’re visualising a supertype, and some but not all subtypes contain specific attributes. We want to return those attributes even though they’re not declared on the parent type.

Links between instances

Data Explorer tables need to minimally offer summaries of what links instances have – these are often the most important properties of your data. So it fires off additional queries to populate link-summary columns:

  • Relation counts (for entities and relations): how many relations of each type an instance participates in, via $rel links ($instance).
  • Link counts (for relations only): how many role-players a relation has, grouped by role, via $rel links ($role: $player).
  • Owner counts (for attributes only): how many entities or relations own this attribute value.

These queries batch all visible instance IDs into a single or block to avoid N+1 query patterns. The total count for the tab label comes from a separate reduce $count = count query that respects the current filter.

On the client side, responses are aggregated into InstanceRow objects keyed by instance ID. Multiple answer rows for the same instance (from multi-valued attributes) are merged, with attribute values collected into arrays and deduplicated.

ℹ️ TypeDB does not require each instance to have a unique key, so Instance IDs (IIDs) are the most reliably way to uniquely identify instances.

Instance Details: Walking the Graph

Clicking an instance opens an InstanceDetailTab that loads the instance’s type, attributes, relations and other links. These are powered by the following TypeQL queries:

  • Attributes: a simple $instance has $attr query, grouped by attribute type.
  • Relations$rel links ($role: $instance); $rel links ($otherRole: $player) — fetching every relation the instance plays a role in, along with all other role-players in those relations. Results are grouped by relation IID, with links sorted so the current instance (“self”) appears last.
  • Link attributes: a batched query fetches attributes for all non-self players, and a second query fetches attributes for the relations themselves. Both use or-batched IID conditions.
  • Owners (for attribute instances): since attributes are identified by value rather than IID, the query matches by value equality: $owner has age 30.

The detail view maintains a breadcrumb trail. Clicking a role-player in a relation opens that player’s detail tab, appending the current location to the breadcrumb list.

Data Explorer: Conclusion

Data Explorer enables an easy experience of browsing the data in a TypeDB database without the user needing to write TypeQL. It is powered exclusively by TypeQL queries. These queries use instance IDs to identify instances. They are performant and achieve all desired goals, including the visualisation of heterogeneous result sets in unified views.

Graph Visualisation

Onto the second part of this post: visualising query results as a graph. TypeDB’s data model is a hypergraph, so this is an immediate and natural desire!

Let’s get into the graph visualiser architecture. The pipeline has four distinct stages:

Stage 1: Logical Answer Construction

When a ConceptRowsQueryResponse arrives from the driver, buildStructuredAnswers() builds a StructuredAnswer[] — one entry per answer row, each carrying a list of DataConstraint objects (DataConstraintIsa, DataConstraintHas, DataConstraintLinks, etc.) that reference DataVertex values (entities, relations, attributes, types, values, or Studio-specific specials like expressions and function calls).

This stage lives in an external TypeScript package @typedb/graph-utils, and can be used freely by developers building their own TypeDB graph tooling.

Stage 2: Builder (Logical → Visual)

Studio’s GraphBuilder extends AbstractGraphBuilder from @typedb/graph-utils — a visitor-style abstract class whose build(answers) method walks every constraint in every answer and dispatches to the appropriate handler: vertex, isa, isaExact, has, links, sub, subExact, owns, relates, plays, expression, function, kind, comparison, is, iid, label, value.

The builder decides:

  • Whether a node should be created (based on whether the variable is in the query’s outputs)
  • What shape and colour to assign (entities are rounded rectangles, relations are diamonds, attributes are ellipses)
  • How to deduplicate nodes across answers (by IID for instances, by type label for types, by composite key for expressions and function calls)
  • How to fall back to a curved edge when multiple constraints connect the same pair of nodes

This is where the AbstractGraphBuilder abstraction pays off. The decoupling lets the visual mapping change without touching the data extraction logic.

Stage 3: Force layout

The GraphVisualiser class owns a Sigma.js instance, a Graphology MultiGraph, and a LayoutWrapper. After the builder populates the graph, the layout engine arranges nodes spatially.

Studio currently uses a d3-force layout wrapped behind the LayoutWrapper interface. It uses D3ForceSupervisor for fresh queries which settles naturally as alpha decays, and a synchronous D3ForceStatic variant for restoring preserved graphs across pane re-attaches. Several other algorithms are implemented in the Layouts factory including ForceAtlas2 and graphology-force. There is quite a bit of potential for offering parameter configuration; it’s hard to find a “one size fits all” solution. Currently we’ve been finding the most consistent results using the Fruchterman-Reingold placement algorithm offered by D3-force.

Stage 4: Sigma rendering

Sigma.js is a library for building performant graphs in JavaScipt. It provides a convenient abstraction over the WebGL API, granting Studio the benefits of hardware acceleration without us needing to write low-level code. Thus Studio needs only take the outputs (node/edge coordinates and metadata) from force layout and map them to Sigma functions.

Sigma only offers circles, so Studio implements three additional shape shaders (rounded rect, diamond, ellipse) under engine/node-programs/ for entities, relations and attributes respectively.

Sigma’s default behaviour for labelling nodes is to place the labels outside nodes, but for Studio we want internal labels. One difficulty here is that Sigma’s default renderLabels function renders labels as their own layer distinct from the drawing layer, whereas logically a labeled node should be an atomic unit. Thus with the default behaviour, overlapping nodes causes all text to render on top of all nodes, which looks strange. Thus, we override renderLabels so labels draw in zIndex order with each node’s drawLabel erasing the canvas under its shape before drawing text. This fixes the issue.

Overlapping nodes also present a problem with transparency: you should never be able to see one node through another. Thus, Studio prefers to blend foreground and background colours to achieve an alpha-like effect when nodes are faded away due to applied highlights.

Another challenge is managing WebGL resource limits. Browsers cap WebGL contexts at ~8–16 per page. On destroy(), Studio explicitly calls WEBGL_lose_context.loseContext() on every canvas before sigma.kill(), so when a user closes a run tab or switches panes, the browser reclaims the context slot immediately. If Studio didn’t do this, graphs would simply stop working after a few have been opened.

The InteractionHandler registers Sigma event listeners for hover, drag, click, double-click, and stage events. Clicking a node selects it and highlights it. However, highlighting just the clicked node is not very useful. It’s more powerful to see its logical connections, which may be a few hops away in TypeDB. So we trigger a kind-aware neighbourhood expansion: entities pull in their attributes, relations pull in their connected players, recursing up to depth 4 to follow relation chains without blowing up. The visualiser also supports text search, which matches nodes by IID, value, type label, or label name.

Edge colouring can be toggled to reflect query constraint indices, giving users a visual mapping between the TypeQL they wrote and the graph they see. Node sizing can optionally scale with degree so hub nodes stand out.

Graph visualisation: Conclusion

TypeDB Studio’s graph visualiser attempts to reinvent as few wheels as possible. By piggybacking on Sigma.js, Graphology and d3-force, we are able to keep the codebase relatively slim while offering a plethora of graph interaction features.

The parts that require the most manual fine-tuning are: the force layout parameters, because it’s hard to find one set of values that works well in all scenarios); and the rendering of nodes and vertices, because Sigma.js is flexible but opinionated in its default graph look and feel.

It also aims to be as composable as possible, in particular via @typedb/graph-utils. A developer could integrate this utility library into their own Web or Python stack and use it to easily spin up a TypeDB graph using any layout and rendering library of their choice.

Try TypeDB Studio

Share this article

TypeDB Newsletter

Stay up to date with the latest TypeDB announcements and events.

Subscribe to newsletter

Further Learning

Feedback