@qiwi/tech-radar
v2.1.0
Published
Fully automated tech-radar generator
Readme
📡 @qiwi/tech-radar
Fully automated tech-radar generator. Two output styles from the same input: a classic Zalando-style radar (built on top of zalando/tech-radar + 11ty) and Aurora — a self-contained pure-SVG renderer with theming, a per-scope snapshot timeline and an optional About page.
Purpose
The Tech Radar is a tool to inspire and support engineering teams at Zalando to pick the best technologies for new projects; it provides a platform to share knowledge and experience in technologies, to reflect on technology decisions and continuously evolve our technology landscape. Based on the pioneering work of ThoughtWorks, our Tech Radar sets out the changes in technologies that are interesting in software development — changes that we think our engineering teams should pay attention to and consider using in their projects.
We've just slightly modified the original implementation for our bloody enterprise requirements.
Demo
Table of contents
Key features
Common (any renderer):
- Reads radar data from
csv,jsonoryaml - Renders one snapshot per
(scope, date)pair, with a separate description page per entry - Auto-derives the
movedindicator across snapshots of the same scope (autoscope) - Redirects each scope URL to its latest snapshot
- CLI / JS / TS API
Per renderer:
zalando— Zalando-style d3 radar via 11ty; a top-level navigation page lists all scopes; templates are user-overridable. Strict 4 sectors × 4 rings (adopt/trial/assess/hold).aurora— pure-SVG, no client-side d3 or runtime; dark/light themes + colour/mono toggle; built-in per-scope snapshot timeline; sidebar legend with cross-highlight; optional Markdown-driven About page. Variable layout — any 2–8 sectors × 2–8 rings.
Requirements
- Node.js >= 22
- macOS / linux
Install
# yarn
yarn add @qiwi/tech-radar
# npm
npm i @qiwi/tech-radarUsage
CLI
# via local dep
techradar --input "/path/to/files/*.{json, csv, yml}" --output /radar
# through npx
npx @qiwi/tech-radar --input "/path/to/files/*.{json, csv, yml}" --output /radarThe table groups options by scope: common ones first, then zalando-specific, then aurora-specific. A renderer-scoped flag is silently ignored when running the other backend.
| Option | Renderer | Description | Default |
|-------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| cwd | | Current working dir | process.cwd() |
| input | | glob pattern to find radar data: csv/json/yml | <cwd>/data/**/*.{json,csv,yml} |
| output | | Output directory | <cwd>/radar |
| autoscope | | identify same-scoped files as subversions of a single radar; derive each entry moved indicator from the previous snapshot of the same scope (auto-trail) | false |
| nav-title | | site / topbar title | 📡 Tech radars |
| nav-footer | | page footer (<footer> on each generated page) | |
| temp | | temporary assets dir | temp-dir + random subfolder |
| renderer | | output backend: zalando (classic d3 radar) or aurora (pure-SVG dark-themed renderer with a built-in snapshot timeline) | zalando |
| favicon | | path to a custom favicon (.ico / .png). Copied to <output>/favicon.ico and overrides the bundled default in both renderers. | |
| base-prefix | zalando | base context for assets. Path-shaped (tech-radar, empty) → relative URLs at any mount; URL-shaped (https://cdn…, //cdn…) → kept as absolute (CDN case). Aurora always emits relative URLs and ignores this. | '/' |
| nav-page | zalando | generate a top-level navigation page listing all scopes. Aurora exposes scopes via the in-radar topbar tabs instead. | false |
| templates | zalando | custom 11ty/nunjucks compatible templates directory. Its contents will be merged into the default templates dir. Aurora is not template-customisable. | |
| about | aurora | path to an .md or .html file with radar overview. When set, aurora renders a global About page at <output>/about/ and surfaces a ? link in the legend footer. Markdown supports h1–h3, paragraphs, unordered lists, **bold**, and [text](url) — anything fancier should be authored as HTML. | |
| credits | aurora | include the generator credit (QIWI ❤ Open Source, with the trailing words linking back to the generator repo) in the legend footer. Set to false to suppress on deployments where it isn't wanted. | true |
| auto-fit-rings | aurora | size ring radii by entry density — the most crowded (sector, ring) cell expands its ring, the rest shrink. Off by default (equal widths); turn on for radars with uneven distributions where dense cells would otherwise cram blips on top of each other. | false |
JS API
import {run} from '@qiwi/tech-radar'
await run({
input : 'data/*.{csv,json,yml}',
output : 'dist',
basePrefix: 'your project',
autoscope: false
})Input examples
{
"meta":{
"title": "tech radar js",
"date": "2021-06-12"
},
"data":[
{
"name": "TypeScript",
"quadrant": "languages-and-frameworks",
"ring": "Adopt",
"description": "Статически типизированный ЖС",
"moved": "1"
},
{
"name": "Nodejs",
"quadrant": "Platforms",
"ring": "Adopt",
"description": "",
"moved": ""
},
{
"name": "codeclimate",
"quadrant": "tools",
"ring": "Trial",
"description": "Статический анализатор кода",
"moved": "0"
},
{
"name": "Гексагональная архитектура",
"quadrant": "Techniques",
"ring": "Assess",
"description": "Унификации контракта интерфейсов различных слоев приложений",
"moved": "-1"
}
],
"quadrantAliases": {
"q1": "languages-and-frameworks",
"q2": "platforms",
"q3": "tools",
"q4": "techniques"
},
"quadrantTitles": {
"q1": "Languages and frameworks",
"q2": "Platforms",
"q3": "Tools",
"q4" :"Techniques"
}
}meta:
title: tech radar js
date: "2021-06-11"
data:
-
name: TypeScript
quadrant: languages-and-frameworks
ring: Adopt
description: Статически типизированный ЖС
moved: 1
-
name: Nodejs
quadrant: Platforms
ring: Adopt
description:
moved:
-
name: codeclimate
quadrant: tools
ring: Trial
description: Статический анализатор кода
moved: 0
-
name: Гексагональная архитектура
quadrant: Techniques
ring: Assess
description: Унификации контракта интерфейсов различных слоев приложений
moved: -1
quadrantAliases:
q1:
- languages-and-frameworks
- lnf
- lang
- framework
q2: platforms
q3: tools
q4: techniques
quadrantTitles:
q1: Languages and frameworks
q2: Platforms
q3: Tools
q4: Techniquestitle
tech radar js
===
date
2021-06-18
===
name, quadrant, ring, description, moved
TypeScript, language, Adopt, "Статически, типизированный ЖС", 1
Nodejs, Platforms, Adopt, ,
codeclimate, tools, Trial, Статический анализатор кода, 0
Гексагональная архитектура, Techniques, Assess, Унификации контракта интерфейсов различных слоев приложений, -1
===
quadrant, alias
q1, language
q1, Languages-and-frameworks
q2, Platforms
q3, Tools
q4, Techniques
===
quadrant, title
q1, Languages and frameworks
q2, Platforms
q3, Tools
q4, TechniquesCI/CD
Follow gh-action usage example:
jobs:
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup NodeJS
uses: actions/setup-node@v5
with:
node-version: 24
cache: npm
- name: Install deps
run: npm ci
- name: Generate
run: npm run generate
# Pushes dist/ to the gh-pages branch via ggcp (no third-party action).
- name: Push to gh-pages
env:
GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }}
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }}
run: |
npx ggcp 'dist>**/*' https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git/gh-pages --message='docs: update tech-radar static'// Zalando — needs base-prefix matching the deployment path and an explicit
// nav-page if you want the scope listing at the root.
"scripts": {
"generate": "node ./src/cli.mjs --renderer zalando --input \"data/**/*.{csv,json,yml}\" --output dist --base-prefix tech-radar --autoscope true --nav-page true && touch dist/.nojekyll"
}
// Aurora — relative URLs, no nav-page (scope tabs are built in); an optional
// --about file enables the overview screen.
"scripts": {
"generate": "node ./src/cli.mjs --renderer aurora --input \"data/**/*.{csv,json,yml}\" --output dist --autoscope true --about data/about.md && touch dist/.nojekyll"
}This repo deploys both renderers side-by-side under one gh-pages site. Each lands in its own subfolder; the root and old per-scope URLs forward to v2 (the maintained default) so any external link to the pre-dual-renderer layout still resolves.
"scripts": {
"generate": "npm run gen:zalando && npm run gen:aurora && npm run gen:redirects && touch dist/.nojekyll",
"gen:zalando": "node ./src/cli.mjs --renderer zalando --input \"data/**/*.{csv,json,yml}\" --output dist/v1 --base-prefix tech-radar/v1 --autoscope true --nav-page true",
"gen:aurora": "node ./src/cli.mjs --renderer aurora --input \"data/**/*.{csv,json,yml}\" --output dist/v2 --autoscope true --about data/about.md",
"gen:redirects": "node scripts/redirects.mjs"
}Notes:
--base-prefix tech-radar/v1matches the deployment path. The classic renderer bakes the prefix into absolute links it builds at runtime, so it has to know the subfolder. Aurora uses relative URLs only and doesn't need a prefix.--about data/about.mdturns on the radar-overview page on the aurora demo (markdown is fine;.htmlfiles are embedded as-is).dist/.nojekyllis created once at the end so gh-pages doesn't run jekyll on the output.scripts/redirects.mjswrites a single meta-refresh stub atdist/index.htmlpointing atv2/, so external links to the gh-pages root keep landing on a working radar.
Customization
Renderers
The same input data can be rendered into two different output styles. Pick one with the renderer option (or --renderer flag) — the rest of the Customization subsections call out which renderer they apply to.
| renderer | Schema | Description |
|-----------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| zalando (default) | 4x4 only | Classic Zalando-style radar built via 11ty + d3. Customisable through renderSettings and a templates directory (see below). Fixed at 4 quadrants × 4 rings (adopt/trial/assess/hold) — radars in any other shape are rejected at dispatch time. |
| aurora | 4x4 + flex | Self-contained pure-SVG renderer. Dark/light themes + colour/mono chroma cycled via a single topbar toggle, deterministic entry placement, built-in per-scope timeline, hover details, sidebar legend, optional About page. Accepts any 2–8 sectors × 2–8 rings layout (see Sectors and rings). No client-side d3, no runtime. |
# Zalando (default) — generate a nav-page and the classic d3 radar
techradar --input "data/**" --output dist --autoscope --nav-page
# Aurora — overview page, no generator credit in the legend
techradar --input "data/**" --output dist --renderer aurora --autoscope \
--about ./docs/about.md --credits falseawait run({
input: 'data/**',
output: 'dist',
renderer: 'aurora',
autoscope: true,
about: './docs/about.md', // optional radar overview, surfaced via the `?` icon
credits: false, // suppress "QIWI ❤ Open Source"
})Sectors and rings
Applies to: aurora for any count; zalando is restricted to the 4×4 case.
Aurora supports variable shapes — 2 to 8 sectors × 2 to 8 rings. The data file declares them via two optional sections; both are mirrored on the existing quadrant,* and ring conventions so legacy radars keep working.
Declare sectors with sector,title rows (id is s1..s8, in order). Aliases follow the same pattern as legacy quadrant,alias:
sector, title
s1, Backend
s2, Frontend
s3, Mobile
s4, Data
s5, Infra
s6, QA
sector, alias
s1, backend-platformDeclare rings with ring,title rows (id is r1..r6, ordered inner → outer):
ring, title
r1, Use
r2, Try
r3, StopThen entries reference either ids or titles (case-insensitive):
name, sector, ring, description, moved
Java, s1, Use, Backend lang, 0
React, frontend, Try, UI library, 1JSON/YAML use the same shape — sectors: [{ id, title, aliases? }] and rings: [{ id, title }].
If the data uses only the legacy quadrant,* sections + standard ring names (adopt, trial, assess, hold), the parser produces both the new sectors/rings arrays AND the legacy quadrantTitles/quadrantAliases view — that radar can be rendered by either backend. The dispatch layer (src/renderer/index.js) rejects flex radars from zalando with a clear error pointing at aurora.
Ring auto-derivation. If a radar has no explicit ring,title section, rings are inferred from the unique values in the entries' ring column. When all match the legacy set, the canonical adopt/trial/assess/hold order is preserved. Otherwise: first-seen order with r1..rN ids.
Group labels
Applies to: any renderer. Every radar document provides its own definition of what each section represents — override the titles per radar. Legacy quadrant,* syntax for the 4-sector case:
quadrant, title
q1, Languages and frameworks
q2, Platforms
q3, Tools
q4, TechniquesNew sector,* syntax for the same effect (or for non-4 layouts):
sector, title
s1, Languages and frameworks
s2, Platforms
s3, Tools
s4, TechniquesRing colors
Applies to: zalando only. The classic renderer reads a renderSettings object that the underlying d3 radar consumes for sector fills, ring labels and canvas size:
{
"svg_id": "radar",
"width": 1450,
"height": 1100,
"colors": {
"background": "#fff",
"grid": "#bbb",
"inactive": "#ddd"
},
"rings": [
{ "name": "ADOPT", "color": "#93c47d", "id": "adopt" },
{ "name": "TRIAL", "color": "#93d2c2", "id": "trial" },
{ "name": "ASSESS", "color": "#fbdb84", "id": "assess" },
{ "name": "HOLD", "color": "#efafa9", "id": "hold" }
],
"print_layout": true
}Aurora ignores renderSettings — its palette is theme-driven (CSS custom properties); see Aurora theming below.
Templates
Applies to: zalando only. For advanced view modification, point the templates option at a directory of njk/11ty files. The directory is merged on top of the bundled templates (matching files override). Expected structure:
assets/
favicon.ico
radar.css
radar.js
_data/
settings.json
_includes/
footer.njk
legend.njk
_layouts/
entries.njk
page.njk
radar.njk
redirect.njk
root.njk
table.njk
entries/
entries.11tydata.json # applies to all entry .md files (layout, tags)
q1/q1.11tydata.json # quadrant index per directory: { "quadrant": 0 }
q2/q2.11tydata.json # ... 1
q3/q3.11tydata.json # ... 2
q4/q4.11tydata.json # ... 3Aurora theming
Applies to: aurora only. The renderer ships its own CSS and JS as static assets (<output>/aurora.css, <output>/aurora.js) and is not template-customisable. To tweak the visuals:
- Global tokens live in
src/renderer/aurora/styles.js— surface colours (--bg,--fg,--accent,--line…), spacing, theme-specific overrides ([data-theme="light"],[data-chroma="mono"]), legend/timeline/topbar styling. - Per-sector colours are computed per radar and emitted as a
<style>block inside the page itself (seerenderPalette()inpages.js). Tokens:--s{N}-accent(labels/strokes),--s{N}-fill(blip body — same as accent on dark, vivid on light+colour),--s{N}-grad-0/1(sector wash gradient stops). The base hue rotates360°/Nbetween sectors starting fromBASE_HUEingeometry.js; changeBASE_HUEto shift the whole palette. - Geometry also lives in
geometry.js—buildRings()lays out M rings evenly acrossMAX_RADIUS,buildSectors()distributes N angular slices. Blip placement constants (MIN_DIST,MAX_ATTEMPTS) are in the same file.
The theme/chroma user choice is persisted in localStorage.aurora-prefs and applied via inline <script> before the stylesheet runs, so there is no FOUC.
Contributing
Feel free to open new issues: bug reports, feature requests or questions. You're always welcome to suggest a PR. Just fork this repo, write some code, add some tests and push your changes. Any feedback is appreciated.
Update the radar data
- Clone the repo:
git clone [email protected]:qiwi/tech-radar.git - Install deps:
npm install - Place a new radar data file to
data/<scope>/<date>.{csv|json|yaml} - Fill it as shown in examples / its siblings
- Run
npm run generate && npm run preview - Follow http://localhost:3000/. Assess the result
- Push commit and create a pull request
Enhance the generator
- Clone the repo:
git clone [email protected]:qiwi/tech-radar.git - Install deps:
npm install - Make some changes in
src/ - Put some tests to
test/ - Run
npm test - Repeat if necessary steps 1 to 3
- Push commit and create a pull request
Alternatives
- https://github.com/thoughtworks/build-your-own-radar
- https://github.com/zalando/tech-radar
- https://www.npmjs.com/package/@backstage/plugin-tech-radar
