Adjacency Tables as Network Graph Alternatives

Permalink to "Adjacency Tables as Network Graph Alternatives"

A network diagram’s equivalent is not a paragraph; it is data. Every graph is a set of nodes and edges, and both can be presented as tables that screen readers navigate well. An edge table (“Checkout calls Auth service”) is complete and filterable. A node table with a “Connects to” column gives a per-node view. Either one, done well, lets screen reader users answer the same questions sighted users answer from the picture — and often lets everyone answer them faster.

This guide belongs to accessible network & graph diagrams.

Spec reference

Permalink to "Spec reference"
  • SC 1.1.1 Non-text Content (A) — complex images need a long description or equivalent; for data graphics, a data table is the most complete equivalent.
  • SC 1.3.1 Info and Relationships (A) — table headers and row headers must be marked up so each cell’s meaning is announced.
  • SC 1.4.1 Use of Color (A) — edge types shown by colour in the graph need words in the table.
  • SC 2.4.6 Headings and Labels (AA) — the table caption says what the table represents and that it mirrors the graph.
An edge table for a service graph Table mock with columns From, Relation, To and Calls per minute; rows show Checkout calls Auth service, Checkout calls Payments API, Payments API writes to Ledger DB, and one row highlighted. An edge table for a service graphFromRelationToCalls/minCheckoutcallsAuth service1,240CheckoutcallsPayments API310Payments APIwrites toLedger DB290Auth servicereads fromUser DB1,180Filtering To = "Auth service" answers "what depends on Auth?" in one step.
Direction and relation are columns, so users can filter by either end.

When to use — and when not to

Permalink to "When to use — and when not to"

Provide an edge table for every network diagram that conveys relationships users must act on — dependency maps, flow networks, organisational links. Provide a node table when users mainly ask per-node questions (“how connected is this account?”), with degree and a list of neighbours.

Do not offer only an adjacency matrix (nodes × nodes grid of ticks) for more than a dozen nodes — screen reader users must cross huge empty grids. Do not offer a table without filtering for large graphs; 2,000 rows of edges without filters is complete but unusable.

The misapplication: a “Data” tab that dumps the graph’s JSON. It is complete and machine-readable, and nearly unusable by ear.

Adjacency matrix versus edge table Comparison: an adjacency matrix is sparse and wide, forcing users across many empty cells; an edge table lists only real relationships and filters by any column. Adjacency matrix versus edge table✗ Adjacency matrix (N × N)42 × 42 = 1,764 cells, mostly emptyDirection hard to readNo room for edge attributesFiltering awkwardGood for tiny graphs only✓ Edge table96 rows, one per edgeColumns: From, Relation, ToColumns for volume, latency, typeFilter From, To or RelationScales with filters

Annotated code example

Permalink to "Annotated code example"
<section aria-labelledby="edges-h">
  <h3 id="edges-h">Service connections (table version of the graph)</h3>
  <label for="edge-filter">Filter by service</label>
  <input id="edge-filter" type="search" aria-describedby="edge-count">
  <p id="edge-count" role="status">96 connections</p>
  <table>
    <caption>Service connections: 96 directed edges between 42 services</caption>   <!-- SC 2.4.6 -->
    <thead>
      <tr>
        <th scope="col">From</th><th scope="col">Relation</th>                      <!-- SC 1.3.1 -->
        <th scope="col">To</th><th scope="col" aria-sort="descending">Calls per minute</th>
        <th scope="col">Type</th>
      </tr>
    </thead>
    <tbody>
      <tr><th scope="row">Checkout</th><td>calls</td><td>Auth service</td><td>1,240</td><td>Synchronous</td></tr>  <!-- type in words (SC 1.4.1) -->
    </tbody>
  </table>
</section>
filter.addEventListener('input', debounce(() => {
  const q = filter.value.trim().toLowerCase();
  const rows = edges.filter(e => !q || e.from.toLowerCase().includes(q) || e.to.toLowerCase().includes(q));
  renderRows(rows);
  count.textContent = `${rows.length} connections${q ? ` involving “${filter.value.trim()}”` : ''}`;
}, 400));

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Event Expected announcement AT deviations
T (next table) “Service connections: 96 directed edges…, table, 97 rows, 5 columns” —
Move across a row “Checkout, From… calls, Relation… Auth service, To” Headers read on column change
Filter “auth” “11 connections involving ‘auth’” —
Sort by Calls per minute Sort state announced See sortable table guidance
Questions and which representation answers them Matrix of questions — what depends on X, what does X depend on, busiest connections, isolated nodes, overall shape — against edge table, node table and text summary. Questions and which representation answers themEdge tableNode tableSummaryWhat depends on X?Filter To = XNeighbour listNoWhat does X depend on?Filter From = XNeighbour listNoBusiest connectionsSort by volumeNoTop threeIsolated nodesNoDegree = 0CountOverall shapeNoDegree distributionYes
Tables answer specific questions; summaries answer shape.

Integration context

Permalink to "Integration context"

Tables are the complete alternative; keyboard traversal of node-link graphs is the exploratory one, and announcing graph structure summaries the overview. Showing and hiding the table follows show-as-table toggles for charts, and sorting follows sortable & filterable data grids.

Gotchas

Permalink to "Gotchas"

Undirected graphs. If edges have no direction, list each edge once and use “connected to” — and let filters match either end.

Multi-edges. Two services may have several relations. Give each its own row rather than concatenating relations in one cell.

Graph and table drift. Generate both from the same model; a hand-maintained table goes stale.

Common mistakes

Permalink to "Common mistakes"
  • Offering an adjacency matrix for large graphs.
  • Omitting direction or relation type.
  • Colour-coded edge types with no Type column.
  • No filter, so the table is complete but impractical.

Framework notes

Permalink to "Framework notes"

Most graph libraries hold the edge list you need: Cytoscape.js cy.edges().map(e => e.data()), D3 force simulations’ links array, vis-network DataSet of edges. Render the table from that data with your grid component; if you use TanStack Table, column filters on From and To give “what depends on X” for free.

const rows = cy.edges().map(e => ({ from: e.source().data('name'), relation: e.data('label'), to: e.target().data('name'), rate: e.data('rate') }));

Worked example

Permalink to "Worked example"

A fraud team’s link-analysis graph showed accounts connected by shared devices, addresses and payments. Analysts using screen readers could not use it. The team added an edge table — From account, Relation (“shares device with”, “sent payment to”), To account, Amount, First seen — with filters on either account and relation type. Analysts now start from a flagged account, filter the table to its edges, and follow suspicious relations by filtering again; the graph remains for sighted colleagues and for presentations.

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Give the graph component a table view generated from its model, with standard columns (From, Relation, To, plus attributes) and built-in filters. Teams should not have to build it per diagram.

FAQ

Permalink to "FAQ"
Edge table or node table?

Edge tables are complete and filterable; node tables suit per-node questions. Offer edge tables by default and add node tables when degree matters.

Is a table enough on its own?

It is a complete equivalent. Adding a summary and traversal makes the graph’s shape and local structure easier to grasp.

How do I show weights or volumes?

As columns, with units in the header, sortable.

Should the table be hidden by default?

It can be behind a toggle or tab, as long as it is easy to find and its control is named clearly.

Can the table be exported?

Yes, and it should be — it is the same data users would analyse elsewhere.

What about hierarchical graphs like org charts?

A tree or nested list is often a better equivalent than an edge table. See the org chart guidance.

How should node attributes appear in an edge table?

Put the most useful ones as columns (team, status, type) for both ends if needed — “From team” and “To team” — so users can filter cross-team connections without looking up each node.

Permalink to "Related"

← Back to Accessible Network & Graph Diagrams