Skip to content

qiwi/tech-radar

Repository files navigation

📡 @qiwi/tech-radar

CI Maintainability Code Coverage npm (scoped)

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

Zalando's answer:

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

Aurora demo Zalando-style demo
v2 — Aurora v1 — Zalando

Table of contents

Key features

Common (any renderer):

  • Reads radar data from csv, json or yaml
  • Renders one snapshot per (scope, date) pair, with a separate description page per entry
  • Auto-derives the moved indicator 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-radar

Usage

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 /radar

The 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
})

JSDoc reference

Input examples

json
{
  "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"
  }
}
yaml
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: Techniques
csv
title
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,         Techniques

CI/CD

Follow gh-action usage example:

publish radar to gh-pages
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'
generator scripts (single renderer)
// 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"
}
generator scripts (dual-renderer demo — this repo)

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/v1 matches 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.md turns on the radar-overview page on the aurora demo (markdown is fine; .html files are embedded as-is).
  • dist/.nojekyll is created once at the end so gh-pages doesn't run jekyll on the output.
  • scripts/redirects.mjs writes a single meta-refresh stub at dist/index.html pointing at v2/, 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 false
await 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-platform

Declare rings with ring,title rows (id is r1..r8, ordered inner → outer):

ring,   title
r1,     Use
r2,     Try
r3,     Stop

Then entries reference either ids or titles (case-insensitive):

name,      sector,   ring,   description,            moved
Java,      s1,       Use,    Backend lang,           0
React,     frontend, Try,    UI library,             1

JSON/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,         Techniques

New sector,* syntax for the same effect (or for non-4 layouts):

sector, title
s1,     Languages and frameworks
s2,     Platforms
s3,     Tools
s4,     Techniques

Ring 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     # ...                                       3

Aurora 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 (see renderPalette() in pages.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 rotates 360°/N between sectors starting from BASE_HUE in geometry.js; change BASE_HUE to shift the whole palette.
  • Geometry also lives in geometry.jsbuildRings() lays out M rings evenly across MAX_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

  1. Clone the repo: git clone git@github.com:qiwi/tech-radar.git
  2. Install deps: npm install
  3. Place a new radar data file to data/<scope>/<date>.{csv|json|yaml}
  4. Fill it as shown in examples / its siblings
  5. Run npm run generate && npm run preview
  6. Follow http://localhost:3000/. Assess the result
  7. Push commit and create a pull request

Enhance the generator

  1. Clone the repo: git clone git@github.com:qiwi/tech-radar.git
  2. Install deps: npm install
  3. Make some changes in src/
  4. Put some tests to test/
  5. Run npm test
  6. Repeat if necessary steps 1 to 3
  7. Push commit and create a pull request

Alternatives

License

MIT

About

Fully automated tech-radar generator

Topics

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors