Skip to main content
View as Markdown

The Matrix Grid

The matrix component — two sets of graph nodes crossed into a grid whose cells are glyphs, with its accessible twin.

The one data component that draws no records. A matrix crosses two sets of graph nodes into a grid whose cells are glyphs rather than words — a mark that says "create and read, not update, not delete" in one shape you can scan a whole column of. It binds a read endpoint, never a table: where chart and kpi aggregate records, a matrix reads a graph's nodes and edges and draws the relationships between them.

Reach for it when the question is which of these reach which of those, and how strongly — permissions across roles, features across plans, coverage across teams. Reach for a table instead when the cells are values a reader needs to read word by word.

Path Kind Values Default Description
label string Accessible name for the grid. Present: the grid is role="img" with this as its aria-label. Absent: the grid is aria-hidden and the accessible twin beneath it carries the facts alone. The twin renders either way.
emptyMessage string Message displayed when the bound graph resolves no cells. Not used for a DEGRADED read, where a source could not be resolved and the grid is missing its contribution rather than empty.

dataSource

Path Kind Values Default Description
dataSource object Graph read-endpoint binding for the matrix grid
dataSource.system object Graph read-endpoint binding: the nodes become the grid’s axes and the edges become its cells
dataSource.system.endpoint string Read endpoint path the grid fetches its graph from, server-side with the caller’s own credentials (e.g. /api/admin/organisation/graph)
dataSource.system.nodesKey string Key of the nodes array in the response envelope (default: 'nodes'). Nodes become the grid’s rows and columns.
dataSource.system.edgesKey string Key of the edges array in the response envelope (default: 'edges'). Edges become the grid’s cells.
dataSource.system.query object Static query params merged into every request to the endpoint

autoSave

Path Kind Values Default Description
autoSave object Configuration for automatic persistence of edits. Applies to table, form, kanban, and calendar components.
autoSave.saveMode enum auto, onBlur, manual Save trigger strategy: 'auto' (debounced), 'onBlur' (field blur), 'manual' (button). Default: 'manual'.
autoSave.autoSaveDebounceMs number Debounce delay for auto-save in milliseconds (default: 500, min: 100)
autoSave.showSaveIndicator boolean Display a save status indicator (Saving... / Saved / Error). Default: true when saveMode is auto or onBlur.
autoSave.saveIndicatorPosition enum inline, toast, toolbar Where the save status indicator appears
Path Kind Values Default Description
search object Shared search bar configuration for data-bound components (table, kanban, calendar)
search.enabled boolean Enable search bar (default: true)
search.placeholder string Search input placeholder text
search.debounceMs number Debounce delay for search input in ms (default: 300)
search.highlight boolean Highlight matched search terms in results (default: false)

rows

Path Kind Values Default Description
rows object One axis of the grid: which node kinds it admits, how it groups them, and how it orders them. The SAME shape for rows and for columns.
rows.kinds array Node kinds admitted onto this axis, as the BOUND GRAPH spells them — open strings, never a platform vocabulary. Omitted: every node the graph returns is admitted.
rows.kinds[] string One node kind, as the bound graph spells it
rows.groupBy string Node field whose value groups this axis into labelled bands (e.g. a resource family). Names a field of the bound graph’s nodes, so it is an open string. Drawn on BOTH axes: rows band into stacked groups, columns into groups of headings. Order within a band is the axis’ own, so sortBy still applies inside a group; a node not carrying the field lands in an unnamed band.
rows.sortBy string Node field this axis orders by. Names a field of the bound graph’s nodes, so it is an open string; nodes that do not carry it keep their source order after those that do.
rows.sortDirection enum asc, desc Direction an axis orders in (default: 'asc'). Closed because a direction is platform vocabulary; the field it sorts on is not.

columns

Path Kind Values Default Description
columns object One axis of the grid: which node kinds it admits, how it groups them, and how it orders them. The SAME shape for rows and for columns.
columns.kinds array Node kinds admitted onto this axis, as the BOUND GRAPH spells them — open strings, never a platform vocabulary. Omitted: every node the graph returns is admitted.
columns.kinds[] string One node kind, as the bound graph spells it
columns.groupBy string Node field whose value groups this axis into labelled bands (e.g. a resource family). Names a field of the bound graph’s nodes, so it is an open string. Drawn on BOTH axes: rows band into stacked groups, columns into groups of headings. Order within a band is the axis’ own, so sortBy still applies inside a group; a node not carrying the field lands in an unnamed band.
columns.sortBy string Node field this axis orders by. Names a field of the bound graph’s nodes, so it is an open string; nodes that do not carry it keep their source order after those that do.
columns.sortDirection enum asc, desc Direction an axis orders in (default: 'asc'). Closed because a direction is platform vocabulary; the field it sorts on is not.

cell

Path Kind Values Default Description
cell object What fills the intersection of a row node and a column node: which edges count, how they are drawn, and the one flag drawn beside them
cell.from string Collection in the response envelope the cells are read from (default: the binding’s edgesKey). An open string: it names a key of the bound graph.
cell.kind string Edge kind admitted as a cell — every other edge is ignored. Names the bound graph’s own edge vocabulary, so it is an open string. Omitted: every edge between an axis pair fills its cell.
cell.glyph enum quadrant, filled, ops-label How a populated cell is drawn: quadrant (four corners, one per ops letter), filled (a solid connected mark), ops-label (the ops letters as text). Closed because the renderer draws each one.
cell.opsField string Edge field carrying the operation letters the glyph reads. Names a field of the bound graph’s edges, so it is an open string. Omitted: no cell has ops, and every populated cell renders filled.
cell.flag object One boolean edge field marked on the glyph and named in the accessible twin, with the word used for it
cell.flag.field string Boolean field on the edge whose truth marks this cell. Names a field of the bound graph’s edges, so it is an open string.
cell.flag.label string The word for a marked cell, used in the accessible twin’s flag column and as the marked cell’s title. There is no rendered legend. Required alongside field: a mark nobody can name is a mark a screen-reader user cannot read.

Every property is optional. A matrix missing its axes renders its empty state; it does not stop your app booting.

Binding a graph

A matrix reads two collections that address each other by id, so its binding names both — unlike the single rowsKey of an ordinary system source. endpoint is required; nodesKey and edgesKey default to nodes and edges, and query merges static parameters into every request.

Nodes become the axes; edges become the cells. Because the read runs as the visitor, they only ever see the part of the graph they could have read themselves — and a matrix on a page they may not read draws nothing rather than someone else's data.

The two axes

rows and columns take the same four keys, so learning one teaches you the other. kinds admits node kinds onto the axis, as the endpoint spells them — omit it and every node is admitted. groupBy bands the axis into labelled groups, drawn on both axes. sortBy orders it, with nodes lacking the field keeping their source order after those that have it, and sortDirection is asc by default.

Neither axis is the special one. A band wraps the rows or columns it holds rather than captioning them from a distance, so the grouping survives a re-order and reads correctly to a screen reader.

Two details follow, on either axis. Ordering within a band is the axis's own order, so sortBy keeps working inside each group rather than being overridden by it, and the bands themselves come in the order their values first appear along the axis. And a node that does not carry the field lands in an unnamed band, which draws no caption — so declaring groupBy never hides a node, it only leaves one ungrouped.

kinds, groupBy and sortBy name things in your endpoint's vocabulary, not Sovrium's. Nothing here is an enum: point a matrix at a different graph and its own node kinds and fields work unchanged.

Filling a cell

cell.from is the collection the cells are read from, defaulting to edgesKey. kind admits one edge kind; omit it and every edge between an axis pair fills its cell. glyph is quadrant, filled or ops-label. opsField names the edge field carrying the operation letters the glyph reads — omit it and every filled cell is filled. flag is { field, label }, one boolean edge field marked on the cell, and both keys are required together.

An intersection with no matching edge still renders as a cell — empty, holding the grid's shape. A row whose cells are all empty is information: it is a thing nothing reaches.

When the glyph cannot express the cell

glyph is one value and a real grid usually needs more than one, because the right mark depends on the shape of the operations, not on the row. So glyph names the cell's primary rendering, and a cell it cannot express steps down rather than losing what it knows:

  1. a cell whose operations the declared glyph can express is drawn in it;
  2. a cell it cannot falls back to ops-label — the letters are printed rather than dropped, because dropping them would make two different grants look identical;
  3. a matching edge carrying no operations renders filled — connected, with nothing further claimed.

One grid declaring glyph: quadrant therefore draws quadrants where the operations have four parts, letters where they do not, and solid marks where there are none. You do not configure that per row; it follows the data.

Accessibility

A grid of glyphs is not readable by itself, so a matrix always renders an accessible twin: a real table beneath the grid with one row per filled cell, naming both ends, the operations and the flag. Nothing switches it off. It is the primary artifact, not a fallback.

label decides how the drawing is announced. Set, the grid is one named figure with role="img"; omitted, the grid is hidden from assistive technology. The twin renders either way. An unnamed figure announced as a figure is noise, so a matrix with no label stays silent and lets the table speak.

flag.label is the word the twin uses for a marked cell, and the marked cell's hover text. That is why it is required alongside flag.field: the table is where a mark is actually read, and an unnamed mark leaves nothing there to read.

Rendering

A matrix is drawn on the server. The grid and its twin are in the first response — no hydration, nothing added to the browser bundle, and readable with scripting off. A page can carry one without paying for a chart library.

app.yaml
pages:
  - name: Access
    path: /access
    components:
      - type: matrix
        dataSource:
          system: { endpoint: /api/admin/organisation/graph }
        rows:
          kinds: [table, page, form, bucket, agent-resource]
          groupBy: family
        columns:
          kinds: [role, team, open]
          sortBy: level
          sortDirection: desc
        cell:
          kind: grant
          glyph: quadrant
          opsField: ops
          flag: { field: viaOpenRung, label: granted to everyone }
        label: Who can do what to what
        emptyMessage: No grants declared.

Behaviour

Accessible twin

  • label present: the grid is role="img" carrying it as the accessible name
  • label absent: the grid is aria-hidden and exposes no accessible name
  • The twin is a real table and renders in BOTH states, never aria-hidden
  • The twin has exactly one row per POPULATED cell — no row for an empty intersection
  • A flagged cell's twin row names the flag with cell.flag.label, and an unflagged row does not
  • The same grid read twice — once as a named figure, once as the table alone — carries the same facts

Cell glyphs

  • cell.kind admits only matching edges; an intersection with no matching edge renders an EMPTY cell
  • glyph: 'quadrant' draws the four-corner mark for a four-letter RCUD grant
  • glyph: 'filled' draws a solid mark and claims nothing about the operations
  • glyph: 'ops-label' prints the ops letters verbatim in the cell
  • A declared quadrant degrades to ops-label for RW / AI, and to filled for an ops-less grant
  • cell.flag marks exactly the cells whose flag field is true, and no others
  • One grid draws quadrants, letters and filled marks side by side, and flags the open-rung cells

Empty and degraded reads

  • An axis admitting nothing renders emptyMessage and no grid
  • Rows present but no matching edge renders the GRID with every cell empty, not the empty state
  • A degraded read names each unresolved source and does NOT render as an empty-but-healthy grid
  • A read the caller is refused does not draw a grid claiming zero grants
  • The four reads stay tellable apart from each other

Grid rendering

  • type: 'matrix' decodes, the page boots, and the grid is in the FIRST response with no island mounted
  • rows.kinds admits exactly the declared node kinds as rows; an undeclared kind is not drawn
  • columns.kinds admits exactly the declared node kinds as columns, including the open rung
  • rows.groupBy bands the rows by a node field, one band per distinct value
  • columns.sortBy + sortDirection: desc orders the columns most-privileged first
  • The read carries the CALLER's credentials: a non-admin gets no grid, not somebody else's
  • columns.groupBy bands the columns by a node field, one data-matrix-column-group per distinct value
  • An operator opens the page and reads a grouped, ordered grid of their own configuration

Schema refusals

  • An empty dataSource.system.endpoint is refused at boot, naming the offending path
  • A cell.glyph outside quadrant / filled / ops-label is refused at boot
  • A DB-table dataSource is refused at boot rather than decoding and rendering nothing
  • A cell.flag with a field and no label is refused at boot
  • An empty rows.kinds list is refused at boot rather than reading as "admit nothing"
  • Every mis-declaration is met at boot, and the valid config next to it still boots

Last updated September 23, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium