A self-contained HTML tool for visualizing a pool of candidates (or items, libraries, vendors, anything) against a set of roles (or dimensions, evaluation criteria) with tiered fit scoring, per-role rollups, an action-priority view, and a written-observations panel.
No build step. No dependencies. One static HTML file plus two JSON files of your data.
The matrix view shows a grid of candidates × roles, each cell colored by fit tier (1 / 2 / 3) with primary-lane indicators, declined and closed states, and per-cell notes that surface in a side drawer.
Two companion views ship alongside:
- Priorities — a three-bucket action plan (in motion / this week / on hold) for whatever next-step actions your situation calls for
- Briefing Notes — per-role tier rollups and free-form observations
You bring two JSON files:
matrix-config.json— the canvas: title, the roles (columns), the section row groupsmatrix-data.json— the population: the candidates, their fits, the action plan, the observations
Edit, refresh, repeat.
- Hiring panels evaluating multiple candidates against role-shapes
- Library / vendor / tool evaluation against project requirements
- Any 2D fit grid where you want a matrix view + per-role rollup + action plan + observations
The included example renders an open source library evaluation. Swap the JSON files to make it your own.
git clone https://github.com/dannybauman/role-alignment-matrix.git
cd role-alignment-matrix
# Copy the example JSONs to the names the HTML loads
cp matrix-config.example.json matrix-config.json
cp matrix-data.example.json matrix-data.json
# Serve locally (any static server works)
python3 -m http.server 8000
# Open http://localhost:8000Edit matrix-config.json and matrix-data.json to render your own situation.
The HTML loads its data via fetch(). Browsers block fetch() from file:// URLs for security, so a local HTTP server is required. Any of these work:
python3 -m http.server 8000
npx serve .
ruby -run -ehttpd . -p 8000index.html static shell, no data
matrix-config.example.json example canvas: roles + sections + page metadata
matrix-data.example.json example population: candidates + fits + rollups + actions + observations
matrix-config.json your canvas (gitignored)
matrix-data.json your population (gitignored)
README.md this file
LICENSE MIT
The non-.example versions are gitignored so your private data doesn't accidentally get committed if you're working in a fork.
Top-level fields:
Role object (one per matrix column):
{
"id": "string — short key, used as the key in candidate.fits",
"name": "string — full label shown in the column header",
"status": "active | prospective",
"shortStatus": "string — sub-label under the role name",
"description": "string — tooltip text",
"link": "string — optional URL. Renders as an Open ↗ link in the role drawer (opens when the column header is clicked). Useful for pointing to a tracking ticket, spec doc, benchmarks, or any external context for the role."
}Section object (one per row group):
{
"id": "string — short key, used as candidate.section",
"name": "string — section header label",
"link": "string — optional URL. Renders as a small ↗ next to the section title. Useful for sections that map to a single tracked thing (a hiring ticket, an open issue, a project tracker). Pool / status sections that don't map to one anchor can omit it."
}locationTiers object (optional, only meaningful when candidates are people and physical location matters for the role):
"locationTiers": {
"label": "string — row label shown in the drawer (default: 'Location tier')",
"modifies": ["roleId", ...], // optional — see below
"1": "string — description for Tier 1",
"2": "string — description for Tier 2",
"3": "string — description for Tier 3"
}When present, candidates with a locationTier value (1, 2, or 3) get a colored tier pill in their drawer. Common pattern: tier 1 = local / preferred, tier 2 = manageable / direct flight, tier 3 = stretch / connecting flight or international. The descriptions are free-form so you can frame the tier rule for your situation (travel access, timezone overlap, in-person availability, etc.).
modifies field — list of role IDs that this locationTier should floor on the fit tier. When set, the tool computes effectiveTier = max(meritTier, locationTier) for each named role at init, mutates the fit cell, and preserves the original under fit._meritTier. The candidate drawer surfaces this as merit Tier X → floored to Tier Y by locationTier Z on affected fits. Useful when location is a hard constraint on a specific role (e.g. an on-site hire, a regional partnership) and you want the matrix to reflect that reality without manually editing every fit cell. Roles not in modifies are unaffected.
The locationTier section is also suppressed in the candidate drawer when the candidate is closed or declined for every role in modifies — showing a top-tier pill on someone you've already rejected is a false signal. Drop a role from modifies to keep the pill visible regardless of fit status.
Top-level fields:
{
"candidates": [ /* candidate objects, see below */ ],
"links": { /* per-name link maps */ },
"roleRollups": { /* per-role tier rollups */ },
"outreach": { /* three-bucket action plan */ },
"observations": [ /* commentary entries */ ]
}Candidate object (one per row):
{
"name": "string — display name",
"section": "string — must match a section.id in config",
"location": { "country": "...", "state": "...", "city": "..." }, // optional, see below
"locationTier": 1, // optional, 1/2/3, requires locationTiers config
"fits": {
"<roleId>": {
"tier": 1, // 1, 2, or 3 — fit strength. omit for closed/declined.
"range": "1-2", // optional — overrides tier display when fit is uncertain
"primary": true, // optional — marks the candidate's primary lane for that role
"note": "string", // optional — short context shown in the drawer
"closed": true, // optional — alternative to tier; renders as a closed cell
"declined": true // optional — candidate-side decline indicator
}
}
}A candidate can have a fit cell for any role they apply to. Omitted role IDs render as empty cells.
location field (optional, candidate-level):
Object with optional country, state, city string fields. When present, the candidate drawer shows a Location row joined as city, state, country skipping any empty fields. If the field is present but every subfield is empty (e.g. "location": {}), the row renders as ? to flag missing-but-expected info — useful for hiring use cases where every candidate should have location captured. Omit the field entirely to suppress the row (e.g. for non-people use cases like library evaluation).
locationTier field (optional, candidate-level):
Integer 1, 2, or 3. Only renders if locationTiers is configured in matrix-config.json. The tier description comes from the config; the pill color reuses the matrix tier palette (Tier 1 = primary, Tier 2 = secondary, Tier 3 = caution).
Links object (per-candidate external links shown in the side drawer):
{
"<candidateName>": {
"site": "https://...",
"github": "https://...",
"linkedin": "https://...",
"greenhouse": "https://..." // any keys are accepted; UI shows them as "site", "github", etc.
}
}roleRollups object (per-role tier breakdown shown in the briefing view):
{
"<roleId>": {
"tier1": ["Name 1", "Name 2 (with parenthetical context)"],
"tier2": [...],
"tier3": [...],
"note": "string — commentary on pool depth and shape"
}
}outreach object (three-bucket action plan shown in the priorities view):
{
"motion": [ { "name": "...", "tag": "...", "rationale": "..." } ],
"week": [ { "priority": 1, "name": "...", "tag": "...", "rationale": "..." } ],
"hold": [ { "name": "...", "tag": "...", "rationale": "..." } ]
}priority on week items is 1-5; lower numbers render first.
observations array (free-form commentary shown in the briefing view):
[
{ "title": "string", "body": "string" }
]- Add or remove roles: edit
matrix-config.jsonrolesarray. Theidfield is what links a role to candidate fits, so if you rename a role you'll need to update the corresponding key in eachcandidate.fitsobject. - Add or remove sections (row groups): edit
matrix-config.jsonsections. Eachcandidate.sectionmust match a sectionid. - Add candidates: append to
matrix-data.jsoncandidates. The minimum isname,section, and at least onefitsentry. - Customize visible labels: title / eyebrow / metaItems / counterLabel are all in
matrix-config.json. Tab labels ("Matrix" / "Priorities" / "Briefing Notes") are hardcoded inindex.htmland can be edited directly if needed.
The HTML is a single self-contained file (CSS + JS inline). No bundler, no build step, no npm. The render code reads from globals that are populated at startup by fetching the two JSON files. The fetch flow lives at the bottom of index.html and is the only place data loading is wired — easy to swap out (e.g., to load from a URL parameter or a remote API) if you want.
MIT.
Originally built as a private internal tool for a recruiting working session, then extracted and genericized for general use. Real data lives in a separate (private) data file; this repo carries only the example.
Created with the assistance of Claude.
{ "title": "string — page title and h1", "eyebrow": "string — small label above the title", "metaItems": ["array", "of", "small bullet items below the title"], "counterLabel": "string — label under the candidate-count / role-count display", "roles": [ /* role objects, see below */ ], "sections": [ /* section objects, see below */ ] }