DR McCulloch← WORK

Matrix · the weft viewer · level of detail, decided by the pixel

Matrix

The Grid That Knows How Small It Is

The matrix viewer built for weft measures its space before it commits to a drawing. It takes the room it has been given, divides by what it must show and picks a face to fit. Given a wall, it renders every group pair as a full cell: tinted field, sized grommet, dashed ring and printed count. Squeezed, whole bands fold into aggregate cells that re-render as mini-heatmaps, then sparklines, then single dots. Nothing is virtualised and nothing is clipped. The same edges stay on screen the whole way down; only the mark gets coarser as the pixels run out. Two gates govern it all: 28 pixels decides whether bands collapse and 40 / 20 pixels decide which face the collapsed cells wear.

Faces
4
Collapse gate
28px
Heatmap floor
40px
Sparkline floor
20px
Relation types
7
Chart libraries
0
Watch it decide ↓

i The cell

The viewer is a square matrix: the same set of groups on both axes, gathered into coloured bands. Where a row meets a column, the cell holds every typed weighted edge that runs from a node in the row group to a node in the column group: enables, challenges, blocks and the rest of the seven-verb vocabulary. There is no separate density statistic. The count of edges in a cell sets both the darkness of its field and the size of its mark, always relative to the busiest visible cell. Filter the matrix and every cell re-inks against the new maximum.

4field tint · α = (8 + 60·√(n/max)) / 255grommet · r = 0.10·c → 0.42·c by √(n/max)dashed ring · a second relation type is presentcount printed when n ≥ 2 · type glyph when r ≥ 5one cell = every edge running row group → column group · colour = the dominant relation type

Seven ways to relate

Every edge is typed and directional. The app draws these on dark chrome and this page re-inks them for paper, so both hexes are given.

  • 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 inked

  • Colour = the dominant relation type in the cell; mixed cells add a dashed ring in the second type's colour.
  • Darkness and size both follow √(count / maxCount). The square root keeps sparse cells legible without letting dense ones shout.
  • maxCount is viewport-relative: it is taken over the cells that survive the current filters, so the ink always uses the full dynamic range.
  • Empty cells draw nothing but a transparent hit-rect, so the paper shows through.
  • Aggregates blend: a collapsed cell's colour is the RGB mean of every edge inside it (compositeEdgeColor).

ii The decision

Auto mode is two gates run in sequence, both in plain pixels. First the viewer asks whether the grid fits at all: subtract the fixed margins from the container, divide the shorter side by the number of groups and compare against 28px. At 28 or above every band stays expanded and every pair gets a full cell. Below it, each multi-band folds into a single aggregated row and column, and only then does the second question arise: how should an aggregate be drawn? Divide the space again, now by the smaller number of effective rows, and read the answer off two more thresholds: 40px or more earns a mini-heatmap, 20–39px a sparkline and below 20 a single circle. A ResizeObserver re-runs the whole judgement on every resize. Drag the window and the matrix changes its mind live.

measure the containersubtract margins 110 · 170 · 70 · 70shorter side ÷ all groups = cell pxcell ≥ 28px ?
yesFULL · one cell per group pair
no → multi-group bands collapseshorter side ÷ effective rows = band-cell px
≥ 40px · MINI-HEATMAP20–39px · SPARKLINE< 20px · CIRCLE
  • You can overrule auto by hand. The toolbar offers Auto · Full · Heatmap · Spark · Circle. Full forces every band open; the three aggregate faces force every multi-group band shut. Auto is the default.
  • Per-band overrides win inside auto. Click an aggregate once and that band alone expands, whatever the pixels say.
  • Single-group bands never collapse; there is nothing to fold.
  • The gates are adjustable: 40 and 20 are store defaults (collapseLodThresholds), user-tunable and persisted between sessions.
  • Nothing is virtualised. The whole matrix is one SVG in a scroll container; scale is handled by shrinking cells and folding bands, never by windowing rows away.

iii The four faces

Each face answers one question: what must survive when the pixels run out? Below, the same band pair (services against risks from the demo corpus) drawn all four ways. Read left to right as the room shrinks: the rings and numerals go first, then the pair identities, until the last face keeps only weight.

FULL auto: group-cell ≥ 28pxkeeps pair identity · dominant + second type · exact count
MINI-HEATMAP auto: band-cell ≥ 40pxkeeps every pair's count as a tinted sub-grid · gives up rings and numerals
SPARKLINE auto: band-cell 20–39pxkeeps the relation-type mix as bars in canonical order · gives up which pair
CIRCLE auto: band-cell < 20pxkeeps magnitude only: one mark, sized and tinted by the total

The specimens are drawn large for legibility; in the instrument the sparkline face lives at 20–39 real pixels and the circle face below 20. The sub-grid inks against its own local maximum (α = 30 + 180·(n/subMax)), so each aggregate uses its full contrast range and you can't compare tint across two of them by eye.

iv The squeeze

A working replica of the decision, run on a deterministic twelve-group corpus of four bands of three groups. Drag the slider to change the room the matrix is given and watch both gates fire; or overrule auto with the same five buttons the real toolbar offers. The rulers underneath show exactly where you are relative to each threshold. Hover or focus any inked cell and a card opens beside it, the pair read in full, with every relation type it holds; on an aggregate the card unfolds the folded pairs one by one.

Presentation
the room the grid is given, after the app's fixed margins

Pass over a cell and it's read back: pair, count and dominant relation.

v The chrome

The four faces are only what gets drawn, but the real component wraps them in a full instrument panel. Everything below ships in it; the demo above only paraphrases the parts it needs.

Pointing at it

  • Tooltips follow the cursor on a 100ms show / 120ms hide debounce, scroll under the wheel and toggle between a short reading and the full per-edge rationale.
  • Hover stripes light the whole row and column; click locks a cell, hover stays transient.
  • Keyboard: arrow keys walk the grid with wraparound, Enter locks, Escape clears lock then selection.
  • Aggregates answer differently: one click re-opens the folded band, double or right click opens the pair as a detail tab.

Reading the field

  • Density bars in the margins, stacked by relation type, outgoing per row on the right and incoming per column below, with totals.
  • Structural holes overlay, dashed outlines on under-connected pairs, an × where no edge exists at all.
  • Flow overlay, net signed weight per cell: above +2, below −2.
  • Density overlay, statistical highlight of cells beyond mean + 1.5σ and a fainter wash below mean − σ.

Thinning it

  • Search is textual and semantic at once; matches expand by BFS ego-hops (0–3) across the live adjacency.
  • Non-matches dim to 0.35 when one side matches and 0.08 when neither; a Filter toggle hides them outright.
  • Filters on edge type, node type, minimum weight, group and band all feed the same pipeline, and the ink re-scales to whatever survives.
  • Everything persists: the LOD override and the two thresholds ride the store between sessions; one Reset returns the instrument to auto.

vi The instrument

The Matrix viewer. One component of weft, the grouped-matrix engine for mapping relations between heterogeneous things. Hand-built SVG inside Next.js 16 and React 19, no charting library. The level-of-detail logic lives in a single hook and the four faces in two renderers; a ResizeObserver feeds them and a persisted store remembers the reader's overrides.

The corpus this instrument was built to hold is woven in its own piece, Weft, the loom reads back. That page covers the corpus and what the map is for; this one stays on the mechanism, down to the thresholds and the formulas.

Thresholds, formulas and mode names verbatim from src/lib/hooks/use-collapse-state.ts (the 28px gate and the 40/20 tiers), src/lib/store.ts (defaults and persistence), src/components/weft/matrix-cell.tsx and aggregated-cell.tsx (the four faces). The demo corpus on this page is synthetic and seeded; the rules it follows are the ones listed above.

← back to the workDR McCulloch · algorithmic frottage