Matrix
The matrix viewer I built for Weft
I built the level-of-detail logic for Weft's matrix viewer in March 2026. Weft is my tool for mapping how unlike things relate to one another as a grouped matrix, written in Next.js 16 and React 19, and the viewer draws its own SVG without a charting library.
Before the viewer draws anything it measures the space it has been given and divides that by the number of groups it has to show. With enough room it draws every pair of groups as a full cell, which is a tinted square with a circle sized by the number of links, a dashed ring when a second relation type is present and the count printed in the middle. As the room shrinks, whole bands of groups fold into single rows and columns, and those folded cells are drawn first as mini-heatmaps, then as sparklines and finally as single circles. At every size all the links stay on screen, with each mark a little coarser as its cell gets smaller. Two thresholds settle which drawing you see: when a cell would be narrower than 28 pixels the bands fold, and once they have folded, a cell of 40 pixels or more becomes a mini-heatmap, one of 20 to 39 a sparkline and anything smaller a circle.
Try the demo βThe cell
The viewer is a square matrix with the same groups on both axes, gathered into coloured bands. The cell where a row meets a column holds every link, each with a type and a weight, that runs from an item in the row's group to an item in the column's group, whether it enables, challenges, blocks or has one of the other four types. I don't calculate a separate density figure. The number of links in a cell sets both how dark its square is and how big its circle is, measured against the busiest cell still visible, so when you filter the matrix every cell is redrawn against the new maximum.
- 1 The square's tint, Ξ± = (8 + 60Β·β(n/max)) / 255
- 2 The circle, with a radius from 0.10 to 0.42 of the cell by β(n/max)
- 3 The count, printed when n β₯ 2, or a type icon in its place when link icons are switched on and the radius is 5 or more
- 4 The dashed ring, drawn when a second relation type is present
The seven relation types
Every link has a type and a direction. The app has a dark background, so on this page I use seven colours of my own, picked so that any two of them can be told apart on paper, with red-green colour blindness too. The hex beside each is the colour the app uses.
- enables#4ecdc4 in the app
- extends#ffe66d in the app
- pivots#a78bfa in the app
- challenges#ff6b6b in the app
- blocks#c0392b in the app
- tension#fd79a8 in the app
- compounds#e87de8 in the app
How a cell is coloured and sized
- A cell takes the colour of its commonest relation type, and a cell holding more than one type adds a dashed ring in the colour of the second.
- Darkness and size both follow β(count / maxCount), and the square root keeps a cell with one or two links visible beside the busiest ones.
- maxCount is taken over the cells left after the current filters, so the whole range from pale to dark is always in use.
- An empty cell draws only a transparent rectangle to catch the pointer, so the paper shows through.
- A folded cell takes the average RGB colour of every link inside it (compositeEdgeColor).
The two thresholds
In Auto mode the viewer runs two tests in turn, both in plain pixels. Every band of two or more groups folds into a single row and column once a cell would be narrower than28px, and the second test, which picks the level of detail for the folded cells, runs only after that fold. A ResizeObserver runs both tests again whenever the container changes size, so if you drag the edge of the window the matrix is redrawn as you go.
- You can overrule Auto by hand from the toolbar by choosing Full, Mini-heatmap, Sparkline or Circle. Full forces every band open and the other three force every band of two or more groups shut. Auto is the default.
- Inside Auto, a band you open by hand stays open: click a folded cell once and that band alone expands, whatever the pixel count.
- A band that holds a single group never folds.
- The second test's thresholds, 40 and 20, are defaults in the store (collapseLodThresholds) that you can change and that persist between sessions. The 28px threshold is a fixed constant in the hook.
- The whole matrix is one SVG in a scroll container with no virtualisation, so at every size the viewer copes with scale by shrinking cells and folding bands.
Four levels of detail
Below, the same pair of bands from the demo data, Services against Risks, is drawn at all four levels. Reading from left to right as the room shrinks, the rings and numbers go first, then which pair is which, until the last level shows only the total.
I've drawn these specimens large so they can be read; in the app a sparkline cell is 20 to 39 real pixels wide and a circle cell less than 20. Each mini-heatmap tints against its own busiest pair (Ξ± = 30 + 180Β·(n/subMax)), so every one uses its full range from pale to dark and the tints of two different mini-heatmaps can't be compared by eye.
Try it
This is a working copy of the two tests, run on made-up data of twelve groups in four bands of three, generated from a fixed seed so it is the same on every visit. Drag the slider to change the room the matrix is given and the drawing changes as each threshold is crossed, or overrule Auto with the same five choices the app's toolbar has. The rulers under the controls show where the current size sits against each threshold. The key above the matrix lists only the marks in the drawing you are looking at. Hover over, tap or tab to any cell with links in it and a card opens beside it listing every relation type in that pair, and on a folded cell the card lists the pairs inside it one by one.
How I built it
The level-of-detail logic is in a single hook and the four levels are drawn by two renderers, with a ResizeObserver supplying the container's size and a persisted store keeping each reader's overrides.
Weft has its own page, which describes the strategy data this viewer was built to hold and what the map is for.
The thresholds, formulas and mode names on this page are copied from the app's source: src/lib/hooks/use-collapse-state.ts for the 28px threshold and the 40 and 20 tiers, src/lib/store.ts for the defaults and what persists, and src/components/weft/matrix-cell.tsx and aggregated-cell.tsx for the four levels. The demo data is made up and generated from a fixed seed, and it follows the same rules.
The rest of the viewer
Around the four levels of detail the app has the controls and overlays listed below. All of them ship in the viewer, and the demo higher up copies only the parts it needs to show the two tests.
Hover, click and keys
- Tooltips follow the cursor, appearing after 100ms and hiding after 120ms. They scroll with the mouse wheel and switch between a short reading and the full rationale recorded for each link.
- Hovering highlights the whole row and column, and clicking a cell locks that highlight in place until you clear it.
- The arrow keys move round the grid and wrap at its edges. Enter locks a cell, and Escape clears the lock first and the selection after it.
- A folded cell responds to clicks in its own way: one click opens the band again, and a double or right click opens that pair of bands in a detail tab.
Overlays
- Bars in the margins show how many links each row sends and each column receives, stacked by relation type, with the outgoing bars on the right, the incoming bars along the bottom and a total for each.
- The structural-holes overlay draws a dashed outline round each pair with few links and an Γ where there are none at all.
- The flow overlay marks the net signed weight of each cell, with β² above +2 and βΌ below β2.
- The density overlay highlights cells more than one and a half standard deviations above the mean (mean + 1.5Ο) and puts a fainter wash over those more than one below it (mean β Ο).
Search and filters
- Search matches on wording and on meaning at the same time, and you can widen the matches by up to three steps along the links, a breadth-first search out from each match.
- A cell fades to 0.35 opacity when only one of its two groups matches and to 0.08 when neither does, and a Filter toggle hides the groups that don't match.
- Filters on link type, item type, minimum weight, group and band are applied together, and the shading rescales to whatever is left.
- The level of detail you pick and the two thresholds are kept between sessions, and a single Reset button returns the viewer to Auto.