npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@qiwi/tech-radar

v2.1.0

Published

Fully automated tech-radar generator

Readme

📡 @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

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

{
  "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: Techniques
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:

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/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..r6, 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 [email protected]: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 [email protected]: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