From c659e6e6d38f63b56fe4b1386bd1195314d2677a Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:10:52 +0100
Subject: [PATCH 01/21] Add initial project files for MkDocs Tech Radar plugin
---
docs/index.md | 3 +++
mkdocs.yml | 8 ++++++++
mkdocs_tech_radar/__init__.py | 7 +++++++
mkdocs_tech_radar/py.typed | 0
pyproject.toml | 23 +++++++++++++++++++++++
5 files changed, 41 insertions(+)
create mode 100644 docs/index.md
create mode 100644 mkdocs.yml
create mode 100644 mkdocs_tech_radar/__init__.py
create mode 100644 mkdocs_tech_radar/py.typed
create mode 100644 pyproject.toml
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..798ab9e
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,3 @@
+# Test Site
+
+This is a test site to verify the tech-radar plugin installation.
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..39f6681
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,8 @@
+site_name: Tech Radar Test
+site_description: Testing the tech-radar plugin
+
+plugins:
+ - tech-radar
+
+nav:
+ - Home: index.md
diff --git a/mkdocs_tech_radar/__init__.py b/mkdocs_tech_radar/__init__.py
new file mode 100644
index 0000000..32559a7
--- /dev/null
+++ b/mkdocs_tech_radar/__init__.py
@@ -0,0 +1,7 @@
+"""
+MkDocs Tech Radar Plugin
+
+A MkDocs plugin to generate a visual Tech Radar as part of your documentation site.
+"""
+
+__version__ = "0.1.0"
diff --git a/mkdocs_tech_radar/py.typed b/mkdocs_tech_radar/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..8360a48
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,23 @@
+[project]
+name = "mkdocs-tech-radar"
+version = "0.1.0"
+description = "A MkDocs plugin to generate a visual Tech Radar."
+authors = [
+ { name = "thatmlopsguy", email = "your@email.com" }
+]
+readme = "README.md"
+license = { file = "LICENSE" }
+requires-python = ">=3.10"
+dependencies = [
+ "mkdocs>=1.4.0"
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest",
+ "mkdocs",
+]
+
+
+[project.entry-points."mkdocs.plugins"]
+tech-radar = "mkdocs_tech_radar.plugin:TechRadarPlugin"
From 613f42cbd73d6a13e92ed54dbf887d5456261f07 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 12:19:31 +0100
Subject: [PATCH 02/21] Add .editorconfig and initial tech radar documentation
---
.editorconfig | 14 ++++++++++++++
docs/tech-radar.md | 2 ++
mkdocs.yml | 8 +++++---
3 files changed, 21 insertions(+), 3 deletions(-)
create mode 100644 .editorconfig
create mode 100644 docs/tech-radar.md
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..8fd90ba
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,14 @@
+# https://editorconfig.org/
+
+root = true
+
+[*]
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+end_of_line = lf
+charset = utf-8
+
+[*.py]
+max_line_length = 80
diff --git a/docs/tech-radar.md b/docs/tech-radar.md
new file mode 100644
index 0000000..3731872
--- /dev/null
+++ b/docs/tech-radar.md
@@ -0,0 +1,2 @@
+# Tech Radar
+
diff --git a/mkdocs.yml b/mkdocs.yml
index 39f6681..b00eda5 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,8 +1,10 @@
site_name: Tech Radar Test
site_description: Testing the tech-radar plugin
-plugins:
- - tech-radar
-
nav:
- Home: index.md
+ - Tech Radar: tech-radar.md
+
+plugins:
+ - search
+ - tech-radar
\ No newline at end of file
From 9337f16bdb240046999a443a0a58567a33177467 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 15:01:28 +0100
Subject: [PATCH 03/21] feat: Implement TechRadarPlugin to generate a visual
Tech Radar based on Zalando's implementation.
- Create radar_inline.html template for inline embedding of the radar.
- Add configuration options for the plugin in mkdocs.yml.
- Include example JSON data file for technology entries.
- Update README.md with detailed usage instructions and features.
- Enhance tech-radar.md with an overview of the Tech Radar concept.
- Add test for plugin import functionality.
---
README.md | 143 +++++-
docs/radars/2025-08-20.json | 283 +++++++++++
docs/tech-radar.md | 23 +
mkdocs.yml | 25 +-
mkdocs_tech_radar/plugin.py | 167 +++++++
mkdocs_tech_radar/templates/radar_inline.html | 456 ++++++++++++++++++
pyproject.toml | 15 +-
tests/test_plugin.py | 5 +
8 files changed, 1112 insertions(+), 5 deletions(-)
create mode 100644 docs/radars/2025-08-20.json
create mode 100644 mkdocs_tech_radar/plugin.py
create mode 100644 mkdocs_tech_radar/templates/radar_inline.html
create mode 100644 tests/test_plugin.py
diff --git a/README.md b/README.md
index 6bbba5c..2de45bf 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,142 @@
-# tech-radar
\ No newline at end of file
+# MkDocs Tech Radar Plugin
+
+A MkDocs plugin that generates an interactive Technology Radar based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
+This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions.
+
+## Features
+
+- 🎯 **Interactive visualization** with D3.js-powered radar chart
+- 🎨 **Zalando Tech Radar design** - proven and widely adopted visual style
+- 📊 **Four-ring assessment model** (Adopt, Trial, Assess, Hold)
+- 🏗️ **Four-quadrant organization** (Languages & Frameworks, Tools, Platforms, Techniques)
+- 🔗 **Clickable entries** with external links support
+- 📱 **Responsive design** that works on all devices
+- 🎭 **Movement indicators** (moved up ▲, moved down ▼, new ★, no change ⬤)
+- 💡 **Hover tooltips** and interactive legend
+- 📋 **Configurable** via JSON data files or plugin configuration
+
+## Installation
+
+Install the plugin using uv, pip, or your preferred Python package manager:
+
+```bash
+# Using uv
+uv pip install mkdocs-tech-radar
+
+# Using pip
+pip install mkdocs-tech-radar
+```
+
+## Configuration
+
+Add the plugin to your `mkdocs.yml`:
+
+```yaml
+plugins:
+ - tech-radar:
+ data_file: 'radars/2025-08-20.json' # Optional: path to JSON data file
+ output_file: 'tech-radar.html' # Optional: output file name (default)
+```
+
+## Usage
+
+### Method 1: JSON Data File
+
+Create a JSON file (e.g., `docs/radars/2025-08-20.json`) with your technology entries:
+
+```json
+{
+ "title": "Our Technology Radar",
+ "date": "2025-08-20",
+ "quadrants": [
+ {"name": "Languages & Frameworks"},
+ {"name": "Tools"},
+ {"name": "Platforms"},
+ {"name": "Techniques"}
+ ],
+ "rings": [
+ {"name": "ADOPT", "color": "#5ba300", "description": "Technologies we have high confidence in"},
+ {"name": "TRIAL", "color": "#009eb0", "description": "Worth pursuing with small risks"},
+ {"name": "ASSESS", "color": "#c7ba00", "description": "Worth exploring with higher risks"},
+ {"name": "HOLD", "color": "#e09b96", "description": "Proceed with caution or avoid"}
+ ],
+ "entries": [
+ {
+ "label": "React",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "A JavaScript library for building user interfaces",
+ "link": "https://reactjs.org/"
+ }
+ ]
+}
+```
+
+### Method 2: Direct Configuration
+
+Configure entries directly in `mkdocs.yml`:
+
+```yaml
+plugins:
+ - tech-radar:
+ title: "Our Technology Radar"
+ date: "2024-11"
+ entries:
+ - label: "React"
+ quadrant: 0 # 0=Languages&Frameworks, 1=Tools, 2=Platforms, 3=Techniques
+ ring: 0 # 0=Adopt, 1=Trial, 2=Assess, 3=Hold
+ moved: 0 # -1=moved out, 0=no change, 1=moved in, 2=new
+ active: true
+ link: "https://reactjs.org/"
+```
+
+## Data Structure
+
+### Entry Properties
+
+- **label** (string): Technology name
+- **quadrant** (0-3): Position in radar
+ - 0: Languages & Frameworks
+ - 1: Tools
+ - 2: Platforms
+ - 3: Techniques
+- **ring** (0-3): Assessment level
+ - 0: ADOPT
+ - 1: TRIAL
+ - 2: ASSESS
+ - 3: HOLD
+- **moved** (integer): Movement indicator
+ - -1: Moved out (▼)
+ - 0: No change (⬤)
+ - 1: Moved in (▲)
+ - 2: New (★)
+- **active** (boolean): Whether entry is clickable/interactive
+- **link** (string, optional): External URL
+- **description** (string, optional): Technology description
+
+### Customization
+
+You can customize the radar appearance by modifying the template or providing custom CSS. The radar uses the Zalando color scheme by default:
+
+- **ADOPT**: #5ba300 (green)
+- **TRIAL**: #009eb0 (teal)
+- **ASSESS**: #c7ba00 (yellow)
+- **HOLD**: #e09b96 (red)
+
+## Example
+
+Check out the included example in the `docs/` directory:
+
+1. Build the documentation: `mkdocs build`
+2. Serve locally: `mkdocs serve`
+3. View the tech radar at: `http://127.0.0.1:8000/tech-radar.html`
+
+## Credits
+
+This plugin is based on the excellent [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation, which in turn is inspired by [ThoughtWorks Technology Radar](https://www.thoughtworks.com/radar).
+
+## License
+
+Apache License 2.0 - see LICENSE file for details.
diff --git a/docs/radars/2025-08-20.json b/docs/radars/2025-08-20.json
new file mode 100644
index 0000000..fc60f15
--- /dev/null
+++ b/docs/radars/2025-08-20.json
@@ -0,0 +1,283 @@
+{
+ "title": "Our Technology Radar",
+ "date": "2025-08-20",
+ "quadrants": [
+ {"name": "Languages & Frameworks"},
+ {"name": "Tools"},
+ {"name": "Platforms"},
+ {"name": "Techniques"}
+ ],
+ "rings": [
+ {"name": "ADOPT", "color": "#5ba300", "description": "Technologies we have high confidence in to serve our purpose, also in large scale. Technologies with a usage culture in our production environment, low risk and recommended to be widely used."},
+ {"name": "TRIAL", "color": "#009eb0", "description": "Technologies that we have seen work with success in project work to solve a real problem; first serious usage experience that confirm benefits and can uncover limitations."},
+ {"name": "ASSESS", "color": "#c7ba00", "description": "Technologies that are promising and have clear potential value-add for us; technologies worth to invest some research and prototyping efforts in to see if it has impact."},
+ {"name": "HOLD", "color": "#e09b96", "description": "Technologies not recommended to be used for new projects. Technologies that we think are not (yet) worth to (further) invest in."}
+ ],
+ "entries": [
+ {
+ "label": "React",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "A JavaScript library for building user interfaces",
+ "link": "https://reactjs.org/"
+ },
+ {
+ "label": "Vue.js",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "The Progressive JavaScript Framework",
+ "link": "https://vuejs.org/"
+ },
+ {
+ "label": "Angular",
+ "quadrant": 0,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Platform for building mobile and desktop web applications",
+ "link": "https://angular.io/"
+ },
+ {
+ "label": "jQuery",
+ "quadrant": 0,
+ "ring": 3,
+ "moved": -1,
+ "active": true,
+ "description": "Fast, small, and feature-rich JavaScript library"
+ },
+ {
+ "label": "TypeScript",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Typed superset of JavaScript that compiles to plain JavaScript",
+ "link": "https://www.typescriptlang.org/"
+ },
+ {
+ "label": "Python",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Programming language that lets you work quickly and integrate systems",
+ "link": "https://www.python.org/"
+ },
+ {
+ "label": "Rust",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "A language empowering everyone to build reliable and efficient software",
+ "link": "https://www.rust-lang.org/"
+ },
+ {
+ "label": "Go",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Open source programming language that makes it easy to build simple software",
+ "link": "https://golang.org/"
+ },
+ {
+ "label": "Docker",
+ "quadrant": 1,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Platform for developing, shipping, and running applications using containers",
+ "link": "https://www.docker.com/"
+ },
+ {
+ "label": "Kubernetes",
+ "quadrant": 1,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Open-source container orchestration system",
+ "link": "https://kubernetes.io/"
+ },
+ {
+ "label": "Terraform",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Infrastructure as Code tool for building, changing, and versioning infrastructure",
+ "link": "https://www.terraform.io/"
+ },
+ {
+ "label": "Jenkins",
+ "quadrant": 1,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Open source automation server for CI/CD",
+ "link": "https://www.jenkins.io/"
+ },
+ {
+ "label": "GitLab CI",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Continuous Integration/Continuous Deployment platform",
+ "link": "https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/"
+ },
+ {
+ "label": "Prometheus",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Open-source monitoring system with a dimensional data model",
+ "link": "https://prometheus.io/"
+ },
+ {
+ "label": "Grafana",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Open source analytics and interactive visualization web application",
+ "link": "https://grafana.com/"
+ },
+ {
+ "label": "AWS",
+ "quadrant": 2,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Amazon Web Services cloud computing platform",
+ "link": "https://aws.amazon.com/"
+ },
+ {
+ "label": "Azure",
+ "quadrant": 2,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Microsoft's cloud computing platform",
+ "link": "https://azure.microsoft.com/"
+ },
+ {
+ "label": "Google Cloud",
+ "quadrant": 2,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Google's suite of cloud computing services",
+ "link": "https://cloud.google.com/"
+ },
+ {
+ "label": "Serverless",
+ "quadrant": 2,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Cloud computing execution model where cloud provider manages server allocation"
+ },
+ {
+ "label": "Edge Computing",
+ "quadrant": 2,
+ "ring": 2,
+ "moved": 2,
+ "active": true,
+ "description": "Distributed computing paradigm that brings computation closer to data sources"
+ },
+ {
+ "label": "Multi-cloud",
+ "quadrant": 2,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Use of multiple cloud computing services in a single architecture"
+ },
+ {
+ "label": "Microservices",
+ "quadrant": 3,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Architectural approach for building applications as a collection of loosely coupled services"
+ },
+ {
+ "label": "DevOps",
+ "quadrant": 3,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Set of practices that combines software development and IT operations"
+ },
+ {
+ "label": "GitOps",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Operational framework that takes DevOps best practices for application development"
+ },
+ {
+ "label": "Infrastructure as Code",
+ "quadrant": 3,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Practice of managing and provisioning computing infrastructure through code"
+ },
+ {
+ "label": "Test-Driven Development",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Software development process relying on software requirements being converted to test cases"
+ },
+ {
+ "label": "Event Sourcing",
+ "quadrant": 3,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Architectural pattern in which changes to application state are stored as a sequence of events"
+ },
+ {
+ "label": "Domain-Driven Design",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Approach to software development that centers the development on programming a domain model"
+ },
+ {
+ "label": "CQRS",
+ "quadrant": 3,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Command Query Responsibility Segregation pattern that separates reads and writes"
+ },
+ {
+ "label": "Machine Learning",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Type of artificial intelligence that allows software applications to learn from data"
+ },
+ {
+ "label": "Blockchain",
+ "quadrant": 3,
+ "ring": 3,
+ "moved": -1,
+ "active": true,
+ "description": "Distributed ledger technology that maintains a continuously growing list of records"
+ }
+ ]
+}
diff --git a/docs/tech-radar.md b/docs/tech-radar.md
index 3731872..66e899e 100644
--- a/docs/tech-radar.md
+++ b/docs/tech-radar.md
@@ -1,2 +1,25 @@
# Tech Radar
+Our technology radar helps us track the technologies we use and evaluate emerging trends in our development ecosystem.
+
+
+
+## About the Tech Radar
+
+The tech radar is a visual tool that categorizes technologies into four rings:
+
+- **ADOPT**: Technologies we have high confidence in and recommend for widespread use
+- **TRIAL**: Technologies worth pursuing with known success cases but some risk
+- **ASSESS**: Promising technologies worth exploring with higher risks
+- **HOLD**: Technologies to proceed with caution or avoid for new projects
+
+Technologies are also organized into four quadrants:
+
+- **Languages & Frameworks**: Programming languages and development frameworks
+- **Tools**: Development and operational tools
+- **Platforms**: Infrastructure and platform technologies
+- **Techniques**: Methods, processes, and architectural patterns
+
+The radar visualization is based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
+
+More info [Thoughtworks Technology Radar](https://www.thoughtworks.com/radar).
diff --git a/mkdocs.yml b/mkdocs.yml
index b00eda5..0f9de10 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,6 +5,29 @@ nav:
- Home: index.md
- Tech Radar: tech-radar.md
+theme:
+ name: "material"
+ palette:
+ - media: "(prefers-color-scheme: light)"
+ scheme: default
+ primary: black
+ accent: amber
+ toggle:
+ icon: material/toggle-switch
+ name: Switch to light mode
+ - media: "(prefers-color-scheme: dark)"
+ scheme: slate
+ primary: black
+ accent: amber
+ toggle:
+ icon: material/toggle-switch-off-outline
+ name: Switch to dark mode
+
plugins:
- search
- - tech-radar
\ No newline at end of file
+ - tech-radar:
+ data_file: 'radars/2025-08-20.json'
+ output_file: 'tech-radar.html'
+ inline_mode: true
+ target_page: 'tech-radar.md'
+
diff --git a/mkdocs_tech_radar/plugin.py b/mkdocs_tech_radar/plugin.py
new file mode 100644
index 0000000..4879ae9
--- /dev/null
+++ b/mkdocs_tech_radar/plugin.py
@@ -0,0 +1,167 @@
+import os
+import json
+from mkdocs.plugins import BasePlugin
+from mkdocs.config import config_options
+from jinja2 import Environment, FileSystemLoader
+
+class TechRadarPlugin(BasePlugin):
+ """MkDocs plugin to generate a visual Tech Radar based on Zalando's implementation."""
+
+ config_scheme = (
+ ('title', config_options.Type(str, default='Technology Radar')),
+ ('date', config_options.Type(str, default='')),
+ ('quadrants', config_options.Type(list, default=[
+ {'name': 'Languages & Frameworks'},
+ {'name': 'Tools'},
+ {'name': 'Platforms'},
+ {'name': 'Techniques'}
+ ])),
+ ('rings', config_options.Type(list, default=[
+ {'name': 'ADOPT', 'color': '#5ba300', 'description': 'We have high confidence in these technologies'},
+ {'name': 'TRIAL', 'color': '#009eb0', 'description': 'Worth pursuing with small risks'},
+ {'name': 'ASSESS', 'color': '#c7ba00', 'description': 'Worth exploring with higher risks'},
+ {'name': 'HOLD', 'color': '#e09b96', 'description': 'Proceed with caution or avoid'}
+ ])),
+ ('entries', config_options.Type(list, default=[])),
+ ('data_file', config_options.Type(str, default='')),
+ ('output_file', config_options.Type(str, default='tech-radar.html')),
+ ('inline_mode', config_options.Type(bool, default=True)),
+ ('target_page', config_options.Type(str, default='tech-radar.md')),
+ )
+
+ def __init__(self):
+ super().__init__()
+ self.radar_content = None
+
+ def on_config(self, config):
+ """Plugin initialization logic."""
+ # Load entries from data file if specified
+ if self.config['data_file']:
+ data_file_path = os.path.join(config['docs_dir'], self.config['data_file'])
+ if os.path.exists(data_file_path):
+ try:
+ with open(data_file_path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ if 'entries' in data:
+ self.config['entries'] = data['entries']
+ # Update other config from file if present
+ for key in ['title', 'date', 'quadrants', 'rings']:
+ if key in data:
+ self.config[key] = data[key]
+ except Exception as e:
+ print(f"Warning: Could not load data file {data_file_path}: {e}")
+
+ # Generate radar content for inline mode
+ if self.config['inline_mode']:
+ self._generate_radar_content()
+
+ return config
+
+ def _generate_radar_content(self):
+ """Generate the radar HTML content for inline embedding."""
+ # Get the template directory
+ template_dir = os.path.join(os.path.dirname(__file__), 'templates')
+
+ # Setup Jinja2 environment
+ env = Environment(loader=FileSystemLoader(template_dir))
+
+ try:
+ template = env.get_template('radar_inline.html')
+ except Exception as e:
+ print(f"Error loading template: {e}")
+ self.radar_content = f"
Error loading radar template: {e}
"
+ return
+
+ # Prepare template context
+ context = {
+ 'title': self.config['title'],
+ 'date': self.config['date'],
+ 'quadrants': self.config['quadrants'],
+ 'rings': self.config['rings'],
+ 'entries': json.dumps(self.config['entries']) if self.config['entries'] else None,
+ }
+
+ # Render the template
+ self.radar_content = template.render(context)
+
+ def on_page_markdown(self, markdown, page, config, files):
+ """Process markdown content to inject tech radar."""
+ print(f"Processing page: {page.file.src_path}, inline_mode: {self.config['inline_mode']}, target: {self.config['target_page']}")
+
+ if not self.config['inline_mode']:
+ return markdown
+
+ # Check if this is the target page for the tech radar
+ if page.file.src_path == self.config['target_page']:
+ print("Found target page, injecting radar content")
+ # Look for the radar placeholder or inject after a specific marker
+ radar_placeholder = ''
+ view_section = '## View the Tech Radar'
+
+ if radar_placeholder in markdown:
+ # Replace placeholder with radar content
+ markdown = markdown.replace(radar_placeholder, self.radar_content or '')
+ print("Replaced placeholder with radar content")
+ elif view_section in markdown:
+ # Replace the "View the Tech Radar" section with the actual radar
+ lines = markdown.split('\n')
+ new_lines = []
+ skip_section = False
+
+ for line in lines:
+ if line.startswith('## View the Tech Radar'):
+ # Replace with radar visualization
+ new_lines.append('## Interactive Tech Radar')
+ new_lines.append('')
+ new_lines.append('')
+ new_lines.append(self.radar_content or '')
+ new_lines.append('
')
+ new_lines.append('')
+ skip_section = True
+ print("Replacing 'View the Tech Radar' section")
+ elif line.startswith('## ') and skip_section:
+ # Found next section, stop skipping
+ skip_section = False
+ new_lines.append(line)
+ elif not skip_section:
+ new_lines.append(line)
+
+ markdown = '\n'.join(new_lines)
+ else:
+ # Add radar at the end if no specific location found
+ markdown += f'\n\n## Interactive Tech Radar\n\n\n{self.radar_content or ""}\n
\n'
+ print("Added radar at the end")
+
+ return markdown
+
+ def on_post_build(self, config):
+ """Generate the tech radar HTML file after the build (for non-inline mode)."""
+ if self.config['inline_mode']:
+ print("Tech Radar embedded inline in documentation")
+ return config
+
+ # Original separate file generation logic
+ template_dir = os.path.join(os.path.dirname(__file__), 'templates')
+ env = Environment(loader=FileSystemLoader(template_dir))
+ template = env.get_template('radar.html')
+
+ context = {
+ 'title': self.config['title'],
+ 'date': self.config['date'],
+ 'quadrants': self.config['quadrants'],
+ 'rings': self.config['rings'],
+ 'entries': json.dumps(self.config['entries']) if self.config['entries'] else None,
+ }
+
+ html_content = template.render(context)
+ output_path = os.path.join(config['site_dir'], self.config['output_file'])
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
+
+ with open(output_path, 'w', encoding='utf-8') as f:
+ f.write(html_content)
+
+ print(f"Tech Radar generated at: {self.config['output_file']}")
+ return config
+
+
+
diff --git a/mkdocs_tech_radar/templates/radar_inline.html b/mkdocs_tech_radar/templates/radar_inline.html
new file mode 100644
index 0000000..175f5f4
--- /dev/null
+++ b/mkdocs_tech_radar/templates/radar_inline.html
@@ -0,0 +1,456 @@
+
+
+
+
+
{{ title }}
+ {% if date %}
{{ date }}
{% endif %}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 8360a48..5877889 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,10 +6,12 @@ authors = [
{ name = "thatmlopsguy", email = "your@email.com" }
]
readme = "README.md"
-license = { file = "LICENSE" }
+license = "Apache-2.0"
requires-python = ">=3.10"
dependencies = [
- "mkdocs>=1.4.0"
+ "mkdocs>=1.4.0",
+ "jinja2>=3.0.0",
+ "mkdocs-material>=9.6.17",
]
[project.optional-dependencies]
@@ -18,6 +20,13 @@ dev = [
"mkdocs",
]
-
[project.entry-points."mkdocs.plugins"]
tech-radar = "mkdocs_tech_radar.plugin:TechRadarPlugin"
+
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools.packages.find]
+include = ["mkdocs_tech_radar*"]
+exclude = ["site*", "tests*"]
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
new file mode 100644
index 0000000..c3b298d
--- /dev/null
+++ b/tests/test_plugin.py
@@ -0,0 +1,5 @@
+def test_plugin_loads():
+ try:
+ from mkdocs_tech_radar.plugin import TechRadarPlugin
+ except ImportError:
+ assert False, "TechRadarPlugin could not be imported"
From 3df510b453ea94fca9e44ba7bc2826e4a7ee6521 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 15:04:44 +0100
Subject: [PATCH 04/21] docs: Add Movement Indicators section to Tech Radar
documentation
---
docs/tech-radar.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/tech-radar.md b/docs/tech-radar.md
index 66e899e..6143ead 100644
--- a/docs/tech-radar.md
+++ b/docs/tech-radar.md
@@ -20,6 +20,15 @@ Technologies are also organized into four quadrants:
- **Platforms**: Infrastructure and platform technologies
- **Techniques**: Methods, processes, and architectural patterns
+## Movement Indicators
+
+Each technology on the radar includes a visual indicator showing its movement trend:
+
+- **▼ Moved out (-1)**: Technology has moved to an outer ring (less recommended)
+- **⬤ No change (0)**: Technology position remains stable
+- **▲ Moved in (1)**: Technology has moved to an inner ring (more recommended)
+- **★ New (2)**: Technology is newly added to the radar
+
The radar visualization is based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
More info [Thoughtworks Technology Radar](https://www.thoughtworks.com/radar).
From 71340a28d77193127ef0a79ede68e67fe758c949 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 15:06:52 +0100
Subject: [PATCH 05/21] refactor: Remove Movement Indicators section from Tech
Radar documentation
---
docs/tech-radar.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/docs/tech-radar.md b/docs/tech-radar.md
index 6143ead..d7aa93c 100644
--- a/docs/tech-radar.md
+++ b/docs/tech-radar.md
@@ -20,8 +20,6 @@ Technologies are also organized into four quadrants:
- **Platforms**: Infrastructure and platform technologies
- **Techniques**: Methods, processes, and architectural patterns
-## Movement Indicators
-
Each technology on the radar includes a visual indicator showing its movement trend:
- **▼ Moved out (-1)**: Technology has moved to an outer ring (less recommended)
From c1cab94e31ed0eba233223fec7274d8b15ec99d8 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 16:25:36 +0100
Subject: [PATCH 06/21] feat: refactor Tech Radar Plugin to Support Multiple
Radar Files and Improve UI
- Modified mkdocs.yml to change plugin configuration, including title and radars folder.
- Enhanced mkdocs_tech_radar/plugin.py to load multiple radar JSON files.
- Updated radar_inline.html template to include dark mode support, improved styling, and a legend for movement indicators.
- Enhanced JavaScript logic for rendering the radar and handling tooltip interactions.
- Added functionality to generate technology lists by quadrant with clickable links for each technology.
---
README.md | 165 ++++-
docs/radars/2025-08-20.json | 4 +-
docs/radars/2025-08-21.json | 282 ++++++++
docs/tech-radar.md | 7 -
mkdocs.yml | 6 +-
mkdocs_tech_radar/plugin.py | 181 +++--
mkdocs_tech_radar/templates/radar_inline.html | 657 ++++++++++++------
7 files changed, 933 insertions(+), 369 deletions(-)
create mode 100644 docs/radars/2025-08-21.json
diff --git a/README.md b/README.md
index 2de45bf..5cc665e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# MkDocs Tech Radar Plugin
A MkDocs plugin that generates an interactive Technology Radar based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
-This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions.
+This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions across multiple radar versions.
## Features
@@ -9,11 +9,11 @@ This plugin helps engineering teams visualize and track their technology adoptio
- 🎨 **Zalando Tech Radar design** - proven and widely adopted visual style
- 📊 **Four-ring assessment model** (Adopt, Trial, Assess, Hold)
- 🏗️ **Four-quadrant organization** (Languages & Frameworks, Tools, Platforms, Techniques)
-- 🔗 **Clickable entries** with external links support
-- 📱 **Responsive design** that works on all devices
- 🎭 **Movement indicators** (moved up ▲, moved down ▼, new ★, no change ⬤)
- 💡 **Hover tooltips** and interactive legend
-- 📋 **Configurable** via JSON data files or plugin configuration
+- ️ **Multiple radar versions** - supports a folder of radar files with dropdown selector
+- 🌙 **Dark mode support** - automatically adapts to MkDocs Material theme
+- 📋 **Inline embedding** - radar is embedded directly in your documentation pages
## Installation
@@ -34,19 +34,35 @@ Add the plugin to your `mkdocs.yml`:
```yaml
plugins:
- tech-radar:
- data_file: 'radars/2025-08-20.json' # Optional: path to JSON data file
- output_file: 'tech-radar.html' # Optional: output file name (default)
+ radars_folder: 'radars' # Optional: folder containing radar JSON files (default: 'radars')
+ target_page: 'tech-radar.md' # Optional: page to embed radar in (default: 'tech-radar.md')
+ title_name: 'Our Tech Radar' # Required: title for the radar (default: 'Technology Radar')
```
## Usage
-### Method 1: JSON Data File
+### Creating Multiple Radar Files
-Create a JSON file (e.g., `docs/radars/2025-08-20.json`) with your technology entries:
+Create multiple JSON files in a `docs/radars/` folder for different radar versions:
+
+```text
+docs/
+ radars/
+ 2025-08-20.json
+ 2025-08-21.json
+ 2025-09-01.json
+```
+
+The plugin will:
+
+- Automatically load all JSON files from the `radars_folder`
+- Display a dropdown selector for switching between radar versions
+- Default to the newest version (based on filename sorting)
+
+Example radar file (`docs/radars/2025-08-20.json`):
```json
{
- "title": "Our Technology Radar",
"date": "2025-08-20",
"quadrants": [
{"name": "Languages & Frameworks"},
@@ -74,28 +90,77 @@ Create a JSON file (e.g., `docs/radars/2025-08-20.json`) with your technology en
}
```
-### Method 2: Direct Configuration
+### Adding Radar to Your Documentation
-Configure entries directly in `mkdocs.yml`:
+Add the radar placeholder in your target markdown page:
-```yaml
-plugins:
- - tech-radar:
- title: "Our Technology Radar"
- date: "2024-11"
- entries:
- - label: "React"
- quadrant: 0 # 0=Languages&Frameworks, 1=Tools, 2=Platforms, 3=Techniques
- ring: 0 # 0=Adopt, 1=Trial, 2=Assess, 3=Hold
- moved: 0 # -1=moved out, 0=no change, 1=moved in, 2=new
- active: true
- link: "https://reactjs.org/"
+```markdown
+# Tech Radar
+
+Our technology radar helps us track the technologies we use and evaluate emerging trends.
+
+
+
+Additional content can go here after the radar.
```
-## Data Structure
+The `` placeholder will be replaced with the interactive radar visualization.
+
+### Title Configuration
+
+- **Required setting**: The radar title is configured exclusively through the `title_name` option in mkdocs.yml
+- **No JSON titles**: The `title` field is no longer required in radar JSON files and will be ignored if present
+- **Consistent branding**: All radar versions display the same title as configured in mkdocs.yml
+
+### Multiple Radar Versions
+
+The plugin provides these features for managing multiple radar versions:
+
+#### Version Selection
+
+- **Dropdown menu**: Appears automatically when multiple radar files are detected
+- **Date-based sorting**: Files are sorted by filename (assuming YYYY-MM-DD format)
+- **Auto-selection**: Newest version is selected by default
+- **Dynamic switching**: Users can switch between radar versions without page reload
+
+#### File Naming Convention
+
+Use date-based naming for optimal sorting:
+
+- `2025-08-20.json` - August 20, 2025 version
+- `2025-09-01.json` - September 1, 2025 version
+- `2025-12-15.json` - December 15, 2025 version
+
+#### Recommended Workflow
+
+1. Create a new radar file for each release/quarter
+2. Copy the previous version as a starting point
+3. Update entries with new technologies and movement indicators
+4. The plugin automatically detects and makes it available
+
+## Configuration Options
+
+| Option | Type | Default | Description |
+|-----------------|--------|-------------------|--------------------------------------------------|
+| `radars_folder` | string | `'radars'` | Folder containing radar JSON files |
+| `target_page` | string | `'tech-radar.md'` | Page to embed the radar in |
+| `title_name` | string | `'Technology Radar'` | Title for the radar (configured in mkdocs.yml) |
+
+## Radar File Structure
+
+Each radar JSON file must contain these required fields:
+
+### Required Fields
+
+- **date** (string): Radar date (e.g., "2025-08-20")
+- **quadrants** (array): Four quadrant definitions
+- **rings** (array): Four ring definitions with colors
+- **entries** (array): Technology entries
### Entry Properties
+Each entry in the `entries` array must have:
+
- **label** (string): Technology name
- **quadrant** (0-3): Position in radar
- 0: Languages & Frameworks
@@ -116,22 +181,64 @@ plugins:
- **link** (string, optional): External URL
- **description** (string, optional): Technology description
-### Customization
+### Example Complete Radar File
-You can customize the radar appearance by modifying the template or providing custom CSS. The radar uses the Zalando color scheme by default:
+```json
+{
+ "date": "2025-08-20",
+ "quadrants": [
+ {"name": "Languages & Frameworks"},
+ {"name": "Tools"},
+ {"name": "Platforms"},
+ {"name": "Techniques"}
+ ],
+ "rings": [
+ {"name": "ADOPT", "color": "#5ba300", "description": "Technologies we have high confidence in"},
+ {"name": "TRIAL", "color": "#009eb0", "description": "Worth pursuing with small risks"},
+ {"name": "ASSESS", "color": "#c7ba00", "description": "Worth exploring with higher risks"},
+ {"name": "HOLD", "color": "#e09b96", "description": "Proceed with caution or avoid"}
+ ],
+ "entries": [
+ {
+ "label": "React",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "A JavaScript library for building user interfaces",
+ "link": "https://reactjs.org/"
+ },
+ {
+ "label": "Vue.js",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Progressive JavaScript framework",
+ "link": "https://vuejs.org/"
+ }
+ ]
+}
+```
+
+## Customization
+
+You can customize the radar appearance by modifying the radar files. The radar uses the Zalando color scheme by default:
- **ADOPT**: #5ba300 (green)
- **TRIAL**: #009eb0 (teal)
- **ASSESS**: #c7ba00 (yellow)
- **HOLD**: #e09b96 (red)
+You can change these colors by modifying the `rings` array in your radar JSON files.
+
## Example
Check out the included example in the `docs/` directory:
-1. Build the documentation: `mkdocs build`
-2. Serve locally: `mkdocs serve`
-3. View the tech radar at: `http://127.0.0.1:8000/tech-radar.html`
+1. Build the documentation: `uv run mkdocs build`
+2. Serve locally: `uv run mkdocs serve`
+3. View the tech radar at: `http://127.0.0.1:8000/tech-radar/`
## Credits
diff --git a/docs/radars/2025-08-20.json b/docs/radars/2025-08-20.json
index fc60f15..b086d09 100644
--- a/docs/radars/2025-08-20.json
+++ b/docs/radars/2025-08-20.json
@@ -1,5 +1,4 @@
{
- "title": "Our Technology Radar",
"date": "2025-08-20",
"quadrants": [
{"name": "Languages & Frameworks"},
@@ -221,7 +220,8 @@
"ring": 1,
"moved": 1,
"active": true,
- "description": "Operational framework that takes DevOps best practices for application development"
+ "description": "Operational framework that takes DevOps best practices for application development",
+ "link": "https://opengitops.dev/"
},
{
"label": "Infrastructure as Code",
diff --git a/docs/radars/2025-08-21.json b/docs/radars/2025-08-21.json
new file mode 100644
index 0000000..53c22c0
--- /dev/null
+++ b/docs/radars/2025-08-21.json
@@ -0,0 +1,282 @@
+{
+ "date": "2025-08-21",
+ "quadrants": [
+ {"name": "Languages & Frameworks"},
+ {"name": "Tools"},
+ {"name": "Platforms"},
+ {"name": "Techniques"}
+ ],
+ "rings": [
+ {"name": "ADOPT", "color": "#5ba300", "description": "Technologies we have high confidence in to serve our purpose, also in large scale. Technologies with a usage culture in our production environment, low risk and recommended to be widely used."},
+ {"name": "TRIAL", "color": "#009eb0", "description": "Technologies that we have seen work with success in project work to solve a real problem; first serious usage experience that confirm benefits and can uncover limitations."},
+ {"name": "ASSESS", "color": "#c7ba00", "description": "Technologies that are promising and have clear potential value-add for us; technologies worth to invest some research and prototyping efforts in to see if it has impact."},
+ {"name": "HOLD", "color": "#e09b96", "description": "Technologies not recommended to be used for new projects. Technologies that we think are not (yet) worth to (further) invest in."}
+ ],
+ "entries": [
+ {
+ "label": "React",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "A JavaScript library for building user interfaces",
+ "link": "https://reactjs.org/"
+ },
+ {
+ "label": "Vue.js",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "The Progressive JavaScript Framework",
+ "link": "https://vuejs.org/"
+ },
+ {
+ "label": "Angular",
+ "quadrant": 0,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Platform for building mobile and desktop web applications",
+ "link": "https://angular.io/"
+ },
+ {
+ "label": "jQuery",
+ "quadrant": 0,
+ "ring": 3,
+ "moved": -1,
+ "active": true,
+ "description": "Fast, small, and feature-rich JavaScript library"
+ },
+ {
+ "label": "TypeScript",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Typed superset of JavaScript that compiles to plain JavaScript",
+ "link": "https://www.typescriptlang.org/"
+ },
+ {
+ "label": "Python",
+ "quadrant": 0,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Programming language that lets you work quickly and integrate systems",
+ "link": "https://www.python.org/"
+ },
+ {
+ "label": "Rust",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "A language empowering everyone to build reliable and efficient software",
+ "link": "https://www.rust-lang.org/"
+ },
+ {
+ "label": "Go",
+ "quadrant": 0,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Open source programming language that makes it easy to build simple software",
+ "link": "https://golang.org/"
+ },
+ {
+ "label": "Docker",
+ "quadrant": 1,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Platform for developing, shipping, and running applications using containers",
+ "link": "https://www.docker.com/"
+ },
+ {
+ "label": "Kubernetes",
+ "quadrant": 1,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Open-source container orchestration system",
+ "link": "https://kubernetes.io/"
+ },
+ {
+ "label": "Terraform",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Infrastructure as Code tool for building, changing, and versioning infrastructure",
+ "link": "https://www.terraform.io/"
+ },
+ {
+ "label": "Jenkins",
+ "quadrant": 1,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Open source automation server for CI/CD",
+ "link": "https://www.jenkins.io/"
+ },
+ {
+ "label": "GitLab CI",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Continuous Integration/Continuous Deployment platform",
+ "link": "https://about.gitlab.com/stages-devops-lifecycle/continuous-integration/"
+ },
+ {
+ "label": "Prometheus",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Open-source monitoring system with a dimensional data model",
+ "link": "https://prometheus.io/"
+ },
+ {
+ "label": "Grafana",
+ "quadrant": 1,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Open source analytics and interactive visualization web application",
+ "link": "https://grafana.com/"
+ },
+ {
+ "label": "AWS",
+ "quadrant": 2,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Amazon Web Services cloud computing platform",
+ "link": "https://aws.amazon.com/"
+ },
+ {
+ "label": "Azure",
+ "quadrant": 2,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Microsoft's cloud computing platform",
+ "link": "https://azure.microsoft.com/"
+ },
+ {
+ "label": "Google Cloud",
+ "quadrant": 2,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Google's suite of cloud computing services",
+ "link": "https://cloud.google.com/"
+ },
+ {
+ "label": "Serverless",
+ "quadrant": 2,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Cloud computing execution model where cloud provider manages server allocation"
+ },
+ {
+ "label": "Edge Computing",
+ "quadrant": 2,
+ "ring": 2,
+ "moved": 2,
+ "active": true,
+ "description": "Distributed computing paradigm that brings computation closer to data sources"
+ },
+ {
+ "label": "Multi-cloud",
+ "quadrant": 2,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Use of multiple cloud computing services in a single architecture"
+ },
+ {
+ "label": "Microservices",
+ "quadrant": 3,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Architectural approach for building applications as a collection of loosely coupled services"
+ },
+ {
+ "label": "DevOps",
+ "quadrant": 3,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Set of practices that combines software development and IT operations"
+ },
+ {
+ "label": "GitOps",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Operational framework that takes DevOps best practices for application development"
+ },
+ {
+ "label": "Infrastructure as Code",
+ "quadrant": 3,
+ "ring": 0,
+ "moved": 0,
+ "active": true,
+ "description": "Practice of managing and provisioning computing infrastructure through code"
+ },
+ {
+ "label": "Test-Driven Development",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Software development process relying on software requirements being converted to test cases"
+ },
+ {
+ "label": "Event Sourcing",
+ "quadrant": 3,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Architectural pattern in which changes to application state are stored as a sequence of events"
+ },
+ {
+ "label": "Domain-Driven Design",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 0,
+ "active": true,
+ "description": "Approach to software development that centers the development on programming a domain model"
+ },
+ {
+ "label": "CQRS",
+ "quadrant": 3,
+ "ring": 2,
+ "moved": 0,
+ "active": true,
+ "description": "Command Query Responsibility Segregation pattern that separates reads and writes"
+ },
+ {
+ "label": "Machine Learning",
+ "quadrant": 3,
+ "ring": 1,
+ "moved": 1,
+ "active": true,
+ "description": "Type of artificial intelligence that allows software applications to learn from data"
+ },
+ {
+ "label": "Blockchain",
+ "quadrant": 3,
+ "ring": 3,
+ "moved": -1,
+ "active": true,
+ "description": "Distributed ledger technology that maintains a continuously growing list of records"
+ }
+ ]
+}
diff --git a/docs/tech-radar.md b/docs/tech-radar.md
index d7aa93c..66e899e 100644
--- a/docs/tech-radar.md
+++ b/docs/tech-radar.md
@@ -20,13 +20,6 @@ Technologies are also organized into four quadrants:
- **Platforms**: Infrastructure and platform technologies
- **Techniques**: Methods, processes, and architectural patterns
-Each technology on the radar includes a visual indicator showing its movement trend:
-
-- **▼ Moved out (-1)**: Technology has moved to an outer ring (less recommended)
-- **⬤ No change (0)**: Technology position remains stable
-- **▲ Moved in (1)**: Technology has moved to an inner ring (more recommended)
-- **★ New (2)**: Technology is newly added to the radar
-
The radar visualization is based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
More info [Thoughtworks Technology Radar](https://www.thoughtworks.com/radar).
diff --git a/mkdocs.yml b/mkdocs.yml
index 0f9de10..a9061b8 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -26,8 +26,8 @@ theme:
plugins:
- search
- tech-radar:
- data_file: 'radars/2025-08-20.json'
- output_file: 'tech-radar.html'
- inline_mode: true
+ title_name: 'Our Custom Mkdocs Technology Radar'
+ radars_folder: 'radars'
target_page: 'tech-radar.md'
+
diff --git a/mkdocs_tech_radar/plugin.py b/mkdocs_tech_radar/plugin.py
index 4879ae9..2725899 100644
--- a/mkdocs_tech_radar/plugin.py
+++ b/mkdocs_tech_radar/plugin.py
@@ -5,60 +5,87 @@
from jinja2 import Environment, FileSystemLoader
class TechRadarPlugin(BasePlugin):
- """MkDocs plugin to generate a visual Tech Radar based on Zalando's implementation."""
+ """MkDocs plugin to generate a visual Tech Radar from multiple radar files."""
config_scheme = (
- ('title', config_options.Type(str, default='Technology Radar')),
- ('date', config_options.Type(str, default='')),
- ('quadrants', config_options.Type(list, default=[
- {'name': 'Languages & Frameworks'},
- {'name': 'Tools'},
- {'name': 'Platforms'},
- {'name': 'Techniques'}
- ])),
- ('rings', config_options.Type(list, default=[
- {'name': 'ADOPT', 'color': '#5ba300', 'description': 'We have high confidence in these technologies'},
- {'name': 'TRIAL', 'color': '#009eb0', 'description': 'Worth pursuing with small risks'},
- {'name': 'ASSESS', 'color': '#c7ba00', 'description': 'Worth exploring with higher risks'},
- {'name': 'HOLD', 'color': '#e09b96', 'description': 'Proceed with caution or avoid'}
- ])),
- ('entries', config_options.Type(list, default=[])),
- ('data_file', config_options.Type(str, default='')),
- ('output_file', config_options.Type(str, default='tech-radar.html')),
- ('inline_mode', config_options.Type(bool, default=True)),
+ ('radars_folder', config_options.Type(str, default='radars')),
('target_page', config_options.Type(str, default='tech-radar.md')),
+ ('title_name', config_options.Type(str, default='Technology Radar')),
)
def __init__(self):
super().__init__()
self.radar_content = None
+ self.radar_files = []
+ self.default_radar = None
def on_config(self, config):
"""Plugin initialization logic."""
- # Load entries from data file if specified
- if self.config['data_file']:
- data_file_path = os.path.join(config['docs_dir'], self.config['data_file'])
- if os.path.exists(data_file_path):
- try:
- with open(data_file_path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- if 'entries' in data:
- self.config['entries'] = data['entries']
- # Update other config from file if present
- for key in ['title', 'date', 'quadrants', 'rings']:
- if key in data:
- self.config[key] = data[key]
- except Exception as e:
- print(f"Warning: Could not load data file {data_file_path}: {e}")
-
- # Generate radar content for inline mode
- if self.config['inline_mode']:
- self._generate_radar_content()
+ # Load all radar files from the radars folder
+ self._load_radar_files(config)
+
+ # Generate radar content for inline embedding
+ self._generate_radar_content()
return config
+ def _load_radar_files(self, config):
+ """Load all radar JSON files from the radars folder."""
+ radars_path = os.path.join(config['docs_dir'], self.config['radars_folder'])
+
+ if not os.path.exists(radars_path):
+ raise ValueError(f"Radars folder not found at {radars_path}. Please create the folder and add radar JSON files.")
+
+ # Find all JSON files in the radars folder
+ json_files = []
+ for file in os.listdir(radars_path):
+ if file.endswith('.json'):
+ json_files.append(file)
+
+ if not json_files:
+ raise ValueError(f"No JSON files found in radars folder {radars_path}. Please add at least one radar JSON file.")
+
+ # Load and parse each radar file
+ radar_data = []
+ for file in json_files:
+ file_path = os.path.join(radars_path, file)
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ # Validate required fields (title is no longer required since we use title_name from config)
+ required_fields = ['date', 'quadrants', 'rings', 'entries']
+ for field in required_fields:
+ if field not in data:
+ raise ValueError(f"Missing required field '{field}' in {file}")
+
+ # Extract filename without extension as the key
+ key = os.path.splitext(file)[0]
+ radar_data.append({
+ 'key': key,
+ 'filename': file,
+ 'data': data
+ })
+ except Exception as e:
+ print(f"Error loading radar file {file_path}: {e}")
+ raise
+
+ # Sort by key (assuming date format YYYY-MM-DD) - newest first
+ radar_data.sort(key=lambda x: x['key'], reverse=True)
+
+ self.radar_files = radar_data
+
+ # Set default radar (newest one)
+ if radar_data:
+ self.default_radar = radar_data[0]['key']
+
+ print(f"Loaded {len(radar_data)} radar files. Default: {self.default_radar}")
+
def _generate_radar_content(self):
"""Generate the radar HTML content for inline embedding."""
+ if not self.radar_files:
+ self.radar_content = "No radar files found. Please add radar JSON files to the radars folder.
"
+ return
+
# Get the template directory
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
@@ -72,13 +99,22 @@ def _generate_radar_content(self):
self.radar_content = f"Error loading radar template: {e}
"
return
+ # Get default radar data
+ default_data = self.radar_files[0]['data']
+
+ # Use title from configuration only
+ title = self.config['title_name']
+
# Prepare template context
context = {
- 'title': self.config['title'],
- 'date': self.config['date'],
- 'quadrants': self.config['quadrants'],
- 'rings': self.config['rings'],
- 'entries': json.dumps(self.config['entries']) if self.config['entries'] else None,
+ 'title': title,
+ 'date': default_data['date'],
+ 'quadrants': default_data['quadrants'],
+ 'rings': default_data['rings'],
+ 'entries': json.dumps(default_data['entries']),
+ 'radar_files': self.radar_files,
+ 'default_radar': self.default_radar,
+ 'radar_files_json': json.dumps(self.radar_files),
}
# Render the template
@@ -86,47 +122,18 @@ def _generate_radar_content(self):
def on_page_markdown(self, markdown, page, config, files):
"""Process markdown content to inject tech radar."""
- print(f"Processing page: {page.file.src_path}, inline_mode: {self.config['inline_mode']}, target: {self.config['target_page']}")
-
- if not self.config['inline_mode']:
- return markdown
+ print(f"Processing page: {page.file.src_path}, target: {self.config['target_page']}")
# Check if this is the target page for the tech radar
if page.file.src_path == self.config['target_page']:
print("Found target page, injecting radar content")
# Look for the radar placeholder or inject after a specific marker
radar_placeholder = ''
- view_section = '## View the Tech Radar'
if radar_placeholder in markdown:
# Replace placeholder with radar content
markdown = markdown.replace(radar_placeholder, self.radar_content or '')
print("Replaced placeholder with radar content")
- elif view_section in markdown:
- # Replace the "View the Tech Radar" section with the actual radar
- lines = markdown.split('\n')
- new_lines = []
- skip_section = False
-
- for line in lines:
- if line.startswith('## View the Tech Radar'):
- # Replace with radar visualization
- new_lines.append('## Interactive Tech Radar')
- new_lines.append('')
- new_lines.append('')
- new_lines.append(self.radar_content or '')
- new_lines.append('
')
- new_lines.append('')
- skip_section = True
- print("Replacing 'View the Tech Radar' section")
- elif line.startswith('## ') and skip_section:
- # Found next section, stop skipping
- skip_section = False
- new_lines.append(line)
- elif not skip_section:
- new_lines.append(line)
-
- markdown = '\n'.join(new_lines)
else:
# Add radar at the end if no specific location found
markdown += f'\n\n## Interactive Tech Radar\n\n\n{self.radar_content or ""}\n
\n'
@@ -135,32 +142,8 @@ def on_page_markdown(self, markdown, page, config, files):
return markdown
def on_post_build(self, config):
- """Generate the tech radar HTML file after the build (for non-inline mode)."""
- if self.config['inline_mode']:
- print("Tech Radar embedded inline in documentation")
- return config
-
- # Original separate file generation logic
- template_dir = os.path.join(os.path.dirname(__file__), 'templates')
- env = Environment(loader=FileSystemLoader(template_dir))
- template = env.get_template('radar.html')
-
- context = {
- 'title': self.config['title'],
- 'date': self.config['date'],
- 'quadrants': self.config['quadrants'],
- 'rings': self.config['rings'],
- 'entries': json.dumps(self.config['entries']) if self.config['entries'] else None,
- }
-
- html_content = template.render(context)
- output_path = os.path.join(config['site_dir'], self.config['output_file'])
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
-
- with open(output_path, 'w', encoding='utf-8') as f:
- f.write(html_content)
-
- print(f"Tech Radar generated at: {self.config['output_file']}")
+ """Post-build processing."""
+ print("Tech Radar embedded inline in documentation")
return config
diff --git a/mkdocs_tech_radar/templates/radar_inline.html b/mkdocs_tech_radar/templates/radar_inline.html
index 175f5f4..2273514 100644
--- a/mkdocs_tech_radar/templates/radar_inline.html
+++ b/mkdocs_tech_radar/templates/radar_inline.html
@@ -1,5 +1,37 @@
{{ title }}
- {% if date %}
{{ date }}
{% endif %}
+ {% if radar_files and radar_files|length > 1 %}
+
+ Version:
+
+ {% for radar in radar_files %}
+
+ {{ radar.data.date }}
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+ ▼
+ Moved out
+
+
+ ⬤
+ No change
+
+
+ ▲
+ Moved in
+
+
+ ★
+ New
+
+
@@ -141,15 +275,57 @@
{{ title }}
From 1b88fd45586d634f5c9a67c1d9a370f5fa4cc209 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 16:49:26 +0100
Subject: [PATCH 07/21] feat: update Tech Radar configuration and improve
plugin functionality
---
.gitignore | 4 +-
.pre-commit-config.yaml | 24 ++++++++++
docs/radars/2025-08-20.json | 2 +-
docs/radars/2025-08-21.json | 2 +-
mkdocs.yml | 2 +-
mkdocs_tech_radar/plugin.py | 81 +++++++++++++++++---------------
pyproject.toml | 94 ++++++++++++++++++++++++++++++++++++-
7 files changed, 163 insertions(+), 46 deletions(-)
create mode 100644 .pre-commit-config.yaml
diff --git a/.gitignore b/.gitignore
index cb0f8dc..5934afe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -182,9 +182,9 @@ cython_debug/
.abstra/
# Visual Studio Code
-# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
+# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
-# and can be added to the global gitignore or merged into this file. However, if you prefer,
+# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..03689e4
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,24 @@
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: check-merge-conflict
+ - id: check-added-large-files
+ - id: trailing-whitespace
+ args: ["--markdown-linebreak-ext=md"]
+ - id: detect-private-key
+ - repo: https://github.com/commitizen-tools/commitizen
+ rev: v4.8.3
+ hooks:
+ - id: commitizen
+ stages: [commit-msg]
+ - repo: https://github.com/codespell-project/codespell
+ rev: v2.4.1
+ hooks:
+ - id: codespell
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.12.9
+ hooks:
+ - id: ruff
+ args: [--fix]
+ - id: ruff-format
diff --git a/docs/radars/2025-08-20.json b/docs/radars/2025-08-20.json
index b086d09..645386f 100644
--- a/docs/radars/2025-08-20.json
+++ b/docs/radars/2025-08-20.json
@@ -1,7 +1,7 @@
{
"date": "2025-08-20",
"quadrants": [
- {"name": "Languages & Frameworks"},
+ {"name": "Frameworks"},
{"name": "Tools"},
{"name": "Platforms"},
{"name": "Techniques"}
diff --git a/docs/radars/2025-08-21.json b/docs/radars/2025-08-21.json
index 53c22c0..ec4d3ca 100644
--- a/docs/radars/2025-08-21.json
+++ b/docs/radars/2025-08-21.json
@@ -1,7 +1,7 @@
{
"date": "2025-08-21",
"quadrants": [
- {"name": "Languages & Frameworks"},
+ {"name": "Frameworks"},
{"name": "Tools"},
{"name": "Platforms"},
{"name": "Techniques"}
diff --git a/mkdocs.yml b/mkdocs.yml
index a9061b8..897dd1d 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -26,7 +26,7 @@ theme:
plugins:
- search
- tech-radar:
- title_name: 'Our Custom Mkdocs Technology Radar'
+ title_name: ''
radars_folder: 'radars'
target_page: 'tech-radar.md'
diff --git a/mkdocs_tech_radar/plugin.py b/mkdocs_tech_radar/plugin.py
index 2725899..fb5d8fb 100644
--- a/mkdocs_tech_radar/plugin.py
+++ b/mkdocs_tech_radar/plugin.py
@@ -1,16 +1,18 @@
-import os
import json
-from mkdocs.plugins import BasePlugin
-from mkdocs.config import config_options
+import os
+
from jinja2 import Environment, FileSystemLoader
+from mkdocs.config import config_options
+from mkdocs.plugins import BasePlugin
+
class TechRadarPlugin(BasePlugin):
"""MkDocs plugin to generate a visual Tech Radar from multiple radar files."""
config_scheme = (
- ('radars_folder', config_options.Type(str, default='radars')),
- ('target_page', config_options.Type(str, default='tech-radar.md')),
- ('title_name', config_options.Type(str, default='Technology Radar')),
+ ("radars_folder", config_options.Type(str, default="radars")),
+ ("target_page", config_options.Type(str, default="tech-radar.md")),
+ ("title_name", config_options.Type(str, default="Technology Radar")),
)
def __init__(self):
@@ -31,52 +33,54 @@ def on_config(self, config):
def _load_radar_files(self, config):
"""Load all radar JSON files from the radars folder."""
- radars_path = os.path.join(config['docs_dir'], self.config['radars_folder'])
+ radars_path = os.path.join(config["docs_dir"], self.config["radars_folder"])
if not os.path.exists(radars_path):
- raise ValueError(f"Radars folder not found at {radars_path}. Please create the folder and add radar JSON files.")
+ raise ValueError(
+ f"Radars folder not found at {radars_path}. Please create the folder and add radar JSON files."
+ )
# Find all JSON files in the radars folder
json_files = []
for file in os.listdir(radars_path):
- if file.endswith('.json'):
+ if file.endswith(".json"):
json_files.append(file)
if not json_files:
- raise ValueError(f"No JSON files found in radars folder {radars_path}. Please add at least one radar JSON file.")
+ raise ValueError(
+ f"No JSON files found in radars folder {radars_path}. Please add at least one radar JSON file."
+ )
# Load and parse each radar file
radar_data = []
for file in json_files:
file_path = os.path.join(radars_path, file)
try:
- with open(file_path, 'r', encoding='utf-8') as f:
+ with open(file_path, encoding="utf-8") as f:
data = json.load(f)
# Validate required fields (title is no longer required since we use title_name from config)
- required_fields = ['date', 'quadrants', 'rings', 'entries']
+ required_fields = ["date", "quadrants", "rings", "entries"]
for field in required_fields:
if field not in data:
- raise ValueError(f"Missing required field '{field}' in {file}")
+ raise ValueError(
+ f"Missing required field '{field}' in {file}"
+ )
# Extract filename without extension as the key
key = os.path.splitext(file)[0]
- radar_data.append({
- 'key': key,
- 'filename': file,
- 'data': data
- })
+ radar_data.append({"key": key, "filename": file, "data": data})
except Exception as e:
print(f"Error loading radar file {file_path}: {e}")
raise
# Sort by key (assuming date format YYYY-MM-DD) - newest first
- radar_data.sort(key=lambda x: x['key'], reverse=True)
+ radar_data.sort(key=lambda x: x["key"], reverse=True)
self.radar_files = radar_data
# Set default radar (newest one)
if radar_data:
- self.default_radar = radar_data[0]['key']
+ self.default_radar = radar_data[0]["key"]
print(f"Loaded {len(radar_data)} radar files. Default: {self.default_radar}")
@@ -87,34 +91,34 @@ def _generate_radar_content(self):
return
# Get the template directory
- template_dir = os.path.join(os.path.dirname(__file__), 'templates')
+ template_dir = os.path.join(os.path.dirname(__file__), "templates")
# Setup Jinja2 environment
env = Environment(loader=FileSystemLoader(template_dir))
try:
- template = env.get_template('radar_inline.html')
+ template = env.get_template("radar_inline.html")
except Exception as e:
print(f"Error loading template: {e}")
self.radar_content = f"
Error loading radar template: {e}
"
return
# Get default radar data
- default_data = self.radar_files[0]['data']
+ default_data = self.radar_files[0]["data"]
# Use title from configuration only
- title = self.config['title_name']
+ title = self.config["title_name"]
# Prepare template context
context = {
- 'title': title,
- 'date': default_data['date'],
- 'quadrants': default_data['quadrants'],
- 'rings': default_data['rings'],
- 'entries': json.dumps(default_data['entries']),
- 'radar_files': self.radar_files,
- 'default_radar': self.default_radar,
- 'radar_files_json': json.dumps(self.radar_files),
+ "title": title,
+ "date": default_data["date"],
+ "quadrants": default_data["quadrants"],
+ "rings": default_data["rings"],
+ "entries": json.dumps(default_data["entries"]),
+ "radar_files": self.radar_files,
+ "default_radar": self.default_radar,
+ "radar_files_json": json.dumps(self.radar_files),
}
# Render the template
@@ -122,17 +126,19 @@ def _generate_radar_content(self):
def on_page_markdown(self, markdown, page, config, files):
"""Process markdown content to inject tech radar."""
- print(f"Processing page: {page.file.src_path}, target: {self.config['target_page']}")
+ print(
+ f"Processing page: {page.file.src_path}, target: {self.config['target_page']}"
+ )
# Check if this is the target page for the tech radar
- if page.file.src_path == self.config['target_page']:
+ if page.file.src_path == self.config["target_page"]:
print("Found target page, injecting radar content")
# Look for the radar placeholder or inject after a specific marker
- radar_placeholder = ''
+ radar_placeholder = ""
if radar_placeholder in markdown:
# Replace placeholder with radar content
- markdown = markdown.replace(radar_placeholder, self.radar_content or '')
+ markdown = markdown.replace(radar_placeholder, self.radar_content or "")
print("Replaced placeholder with radar content")
else:
# Add radar at the end if no specific location found
@@ -145,6 +151,3 @@ def on_post_build(self, config):
"""Post-build processing."""
print("Tech Radar embedded inline in documentation")
return config
-
-
-
diff --git a/pyproject.toml b/pyproject.toml
index 5877889..12e2be5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,13 +11,11 @@ requires-python = ">=3.10"
dependencies = [
"mkdocs>=1.4.0",
"jinja2>=3.0.0",
- "mkdocs-material>=9.6.17",
]
[project.optional-dependencies]
dev = [
"pytest",
- "mkdocs",
]
[project.entry-points."mkdocs.plugins"]
@@ -30,3 +28,95 @@ build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
include = ["mkdocs_tech_radar*"]
exclude = ["site*", "tests*"]
+
+[dependency-groups]
+dev = [
+ "prek>=0.1.1",
+ "ruff>=0.12.9",
+]
+
+[tool.ruff]
+# Exclude a variety of commonly ignored directories.
+exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+]
+
+# Same as Black.
+line-length = 88
+indent-width = 4
+
+# Assume Python 3.10
+target-version = "py310"
+
+[tool.ruff.lint]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
+# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
+# McCabe complexity (`C901`) by default.
+select = [
+ "E4",
+ "E7",
+ "E9",
+ "F",
+ "I", # isort
+ "UP", # pyupgrade
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "PIE", # flake8-pie
+ "SIM", # flake8-simplify
+ "RET", # flake8-return
+ "PTH", # flake8-use-pathlib
+]
+
+ignore = []
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[tool.ruff.format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
+
+# Enable auto-formatting of code examples in docstrings. Markdown,
+# reStructuredText code/literal blocks and doctests are all supported.
+docstring-code-format = false
+
+# Set the line length limit used for formatting and linting.
+docstring-code-line-length = "dynamic"
From 49a5b2bbb1a0d5127fa6e176a47e352459df871a Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 16:50:54 +0100
Subject: [PATCH 08/21] feat: add commitizen as a development dependency
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 12e2be5..b32f17b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ exclude = ["site*", "tests*"]
[dependency-groups]
dev = [
+ "commitizen>=4.8.3",
"prek>=0.1.1",
"ruff>=0.12.9",
]
From 0504d915c52ab5292adb11239ef2935f7a543609 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Wed, 20 Aug 2025 17:10:19 +0100
Subject: [PATCH 09/21] refactor: improve README and plugin code structure;
enhance test for plugin loading
---
README.md | 27 +++++++++---------------
mkdocs_tech_radar/plugin.py | 21 +++++++++----------
pyproject.toml | 42 +++++++++++++++++++++++++++++++++++++
tests/test_plugin.py | 9 ++++++--
4 files changed, 69 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index 5cc665e..f86bd73 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,11 @@ This plugin helps engineering teams visualize and track their technology adoptio
## Features
- 🎯 **Interactive visualization** with D3.js-powered radar chart
-- 🎨 **Zalando Tech Radar design** - proven and widely adopted visual style
- 📊 **Four-ring assessment model** (Adopt, Trial, Assess, Hold)
-- 🏗️ **Four-quadrant organization** (Languages & Frameworks, Tools, Platforms, Techniques)
+- 🏗️ **Four-quadrant organization** (Frameworks, Tools, Platforms, Techniques)
- 🎭 **Movement indicators** (moved up ▲, moved down ▼, new ★, no change ⬤)
- 💡 **Hover tooltips** and interactive legend
-- ️ **Multiple radar versions** - supports a folder of radar files with dropdown selector
+- ️🎨 **Multiple radar versions** - supports a folder of radar files with dropdown selector
- 🌙 **Dark mode support** - automatically adapts to MkDocs Material theme
- 📋 **Inline embedding** - radar is embedded directly in your documentation pages
@@ -34,9 +33,9 @@ Add the plugin to your `mkdocs.yml`:
```yaml
plugins:
- tech-radar:
- radars_folder: 'radars' # Optional: folder containing radar JSON files (default: 'radars')
- target_page: 'tech-radar.md' # Optional: page to embed radar in (default: 'tech-radar.md')
- title_name: 'Our Tech Radar' # Required: title for the radar (default: 'Technology Radar')
+ title_name: 'Our Tech Radar' # Optional: title for the radar (default: 'Technology Radar')
+ radars_folder: 'radars' # Optional: folder containing radar JSON files (default: 'radars')
+ target_page: 'tech-radar.md' # Optional: page to embed radar in (default: 'tech-radar.md')
```
## Usage
@@ -106,12 +105,6 @@ Additional content can go here after the radar.
The `` placeholder will be replaced with the interactive radar visualization.
-### Title Configuration
-
-- **Required setting**: The radar title is configured exclusively through the `title_name` option in mkdocs.yml
-- **No JSON titles**: The `title` field is no longer required in radar JSON files and will be ignored if present
-- **Consistent branding**: All radar versions display the same title as configured in mkdocs.yml
-
### Multiple Radar Versions
The plugin provides these features for managing multiple radar versions:
@@ -140,11 +133,11 @@ Use date-based naming for optimal sorting:
## Configuration Options
-| Option | Type | Default | Description |
-|-----------------|--------|-------------------|--------------------------------------------------|
-| `radars_folder` | string | `'radars'` | Folder containing radar JSON files |
-| `target_page` | string | `'tech-radar.md'` | Page to embed the radar in |
-| `title_name` | string | `'Technology Radar'` | Title for the radar (configured in mkdocs.yml) |
+| Option | Type | Default | Description |
+|-----------------|--------|----------------------|------------------------------------|
+| `title_name` | string | `'Technology Radar'` | Title for the radar |
+| `target_page` | string | `'tech-radar.md'` | Page to embed the radar in |
+| `radars_folder` | string | `'radars'` | Folder containing radar JSON files |
## Radar File Structure
diff --git a/mkdocs_tech_radar/plugin.py b/mkdocs_tech_radar/plugin.py
index fb5d8fb..3dcd67e 100644
--- a/mkdocs_tech_radar/plugin.py
+++ b/mkdocs_tech_radar/plugin.py
@@ -1,5 +1,5 @@
import json
-import os
+from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from mkdocs.config import config_options
@@ -32,19 +32,18 @@ def on_config(self, config):
return config
def _load_radar_files(self, config):
- """Load all radar JSON files from the radars folder."""
- radars_path = os.path.join(config["docs_dir"], self.config["radars_folder"])
+ radars_path = Path(config["docs_dir"]) / self.config["radars_folder"]
- if not os.path.exists(radars_path):
+ if not Path(radars_path).exists():
raise ValueError(
f"Radars folder not found at {radars_path}. Please create the folder and add radar JSON files."
)
# Find all JSON files in the radars folder
json_files = []
- for file in os.listdir(radars_path):
- if file.endswith(".json"):
- json_files.append(file)
+ for file_path in Path(radars_path).iterdir():
+ if file_path.is_file() and file_path.suffix == ".json":
+ json_files.append(file_path.name)
if not json_files:
raise ValueError(
@@ -54,9 +53,9 @@ def _load_radar_files(self, config):
# Load and parse each radar file
radar_data = []
for file in json_files:
- file_path = os.path.join(radars_path, file)
+ file_path = Path(radars_path) / file
try:
- with open(file_path, encoding="utf-8") as f:
+ with Path.open(file_path, encoding="utf-8") as f:
data = json.load(f)
# Validate required fields (title is no longer required since we use title_name from config)
required_fields = ["date", "quadrants", "rings", "entries"]
@@ -67,7 +66,7 @@ def _load_radar_files(self, config):
)
# Extract filename without extension as the key
- key = os.path.splitext(file)[0]
+ key = Path(file).stem
radar_data.append({"key": key, "filename": file, "data": data})
except Exception as e:
print(f"Error loading radar file {file_path}: {e}")
@@ -91,7 +90,7 @@ def _generate_radar_content(self):
return
# Get the template directory
- template_dir = os.path.join(os.path.dirname(__file__), "templates")
+ template_dir = Path(__file__).parent / "templates"
# Setup Jinja2 environment
env = Environment(loader=FileSystemLoader(template_dir))
diff --git a/pyproject.toml b/pyproject.toml
index b32f17b..e23d5b2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -121,3 +121,45 @@ docstring-code-format = false
# Set the line length limit used for formatting and linting.
docstring-code-line-length = "dynamic"
+
+[tool.pytest.ini_options]
+# Test discovery
+testpaths = ["tests"]
+python_files = ["test_*.py", "*_test.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+
+# Output options
+addopts = [
+ "-v", # verbose output
+ "--tb=short", # shorter traceback format
+ "--strict-markers", # strict marker usage
+ "--strict-config", # strict configuration
+]
+
+# Markers for organizing tests
+markers = [
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
+ "integration: marks tests as integration tests",
+ "unit: marks tests as unit tests",
+]
+
+# Minimum version requirement
+minversion = "6.0"
+
+# Paths to ignore during test collection
+norecursedirs = [
+ ".*",
+ "build",
+ "dist",
+ "*.egg",
+ "venv",
+ ".venv",
+ "node_modules",
+]
+
+# Filter warnings
+filterwarnings = [
+ "ignore::UserWarning",
+ "ignore::DeprecationWarning",
+]
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index c3b298d..69337fe 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -1,5 +1,10 @@
def test_plugin_loads():
try:
from mkdocs_tech_radar.plugin import TechRadarPlugin
- except ImportError:
- assert False, "TechRadarPlugin could not be imported"
+
+ # Verify the plugin class exists and can be instantiated
+ assert TechRadarPlugin is not None
+ plugin = TechRadarPlugin()
+ assert plugin is not None
+ except ImportError as e:
+ raise AssertionError("TechRadarPlugin could not be imported") from e
From 0b3e73b2fef8aac2eb56c4dc706bcb3ebfb0322a Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 15:07:24 +0100
Subject: [PATCH 10/21] docs: update index.md to clarify mkdocs configuration
limitations
---
docs/index.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/index.md b/docs/index.md
index 798ab9e..abfc817 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,3 +1,9 @@
# Test Site
This is a test site to verify the tech-radar plugin installation.
+
+
+
+
From f8d8aed4fd1897af93ee5da919b5b563bdafbdbb Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 15:08:08 +0100
Subject: [PATCH 11/21] feat: enhance radar legend with filtering functionality
and improve item styling
---
mkdocs_tech_radar/templates/radar_inline.html | 129 ++++++++++++++++--
1 file changed, 119 insertions(+), 10 deletions(-)
diff --git a/mkdocs_tech_radar/templates/radar_inline.html b/mkdocs_tech_radar/templates/radar_inline.html
index 2273514..0900e86 100644
--- a/mkdocs_tech_radar/templates/radar_inline.html
+++ b/mkdocs_tech_radar/templates/radar_inline.html
@@ -54,13 +54,14 @@
transform: translateX(-50%);
background: var(--radar-bg);
border: 1px solid var(--radar-border);
- border-radius: 6px;
- padding: 8px 12px;
+ border-radius: 8px;
+ padding: 8px 16px;
display: flex;
- gap: 15px;
- font-size: 12px;
+ gap: 12px;
+ font-size: 11px;
color: var(--text-primary);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ white-space: nowrap;
}
[data-md-color-scheme="slate"] .radar-legend-inline {
@@ -70,11 +71,39 @@
.legend-item-inline {
display: flex;
align-items: center;
- gap: 3px;
+ gap: 4px;
+ cursor: pointer;
+ padding: 6px 8px;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+ white-space: nowrap;
+ }
+
+ .legend-item-inline:hover {
+ background-color: rgba(128, 128, 128, 0.1);
+ }
+
+ .legend-item-inline.active {
+ background-color: rgba(0, 123, 204, 0.2);
+ border: 1px solid rgba(0, 123, 204, 0.5);
}
.legend-icon-inline {
- font-size: 14px;
+ font-size: 12px;
+ line-height: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 12px;
+ }
+
+ .entry.highlighted {
+ stroke-width: 4px !important;
+ filter: drop-shadow(0 0 6px rgba(0, 0, 0, 0.8));
+ }
+
+ .entry.dimmed {
+ opacity: 0.3;
}
.quadrant text {
@@ -248,19 +277,19 @@
{{ title }}
-
+
▼
Moved out
-
+
⬤
No change
-
+
▲
Moved in
-
+
★
New
@@ -313,8 +342,15 @@
{{ title }}
config = allRadarData[radarKey];
config.title = originalTitle; // Restore the title from mkdocs.yml
currentRadar = radarKey;
+
+ // Reset filter state when switching radars
+ currentFilter = null;
+ const legendItems = document.querySelectorAll('.legend-item-inline');
+ legendItems.forEach(li => li.classList.remove('active'));
+
renderRadar();
generateTechLists();
+ setupLegendFiltering();
}
}
@@ -573,6 +609,78 @@
{{ title }}
console.log("Tech Radar rendered with", config.entries.length, "entries");
}
+ // Legend filtering functionality
+ let currentFilter = null;
+
+ function setupLegendFiltering() {
+ const legendItems = document.querySelectorAll('.legend-item-inline');
+
+ // Remove existing event listeners by cloning and replacing nodes
+ legendItems.forEach(item => {
+ const newItem = item.cloneNode(true);
+ item.parentNode.replaceChild(newItem, item);
+ });
+
+ // Get fresh references to the legend items
+ const newLegendItems = document.querySelectorAll('.legend-item-inline');
+
+ newLegendItems.forEach(item => {
+ item.addEventListener('click', function() {
+ const movement = parseInt(this.getAttribute('data-movement'));
+
+ // Toggle filter if clicking the same item
+ if (currentFilter === movement) {
+ currentFilter = null;
+ newLegendItems.forEach(li => li.classList.remove('active'));
+ highlightEntries(null);
+ } else {
+ currentFilter = movement;
+ newLegendItems.forEach(li => li.classList.remove('active'));
+ this.classList.add('active');
+ highlightEntries(movement);
+ }
+ });
+ });
+ }
+
+ function highlightEntries(filterMovement) {
+ const entries = svg.selectAll('.entry');
+
+ if (filterMovement === null) {
+ // Remove all highlighting
+ entries.classed('highlighted', false);
+ entries.classed('dimmed', false);
+ // Reset stroke to original ring color
+ entries.each(function(d) {
+ const entry = d3.select(this);
+ const ring = config.rings[d.ring];
+ const shape = entry.select('polygon, circle');
+ shape.attr('stroke', '#fff');
+ });
+ } else {
+ // Apply highlighting based on filter
+ entries.each(function(d) {
+ const entry = d3.select(this);
+ const ring = config.rings[d.ring];
+ const shape = entry.select('polygon, circle');
+
+ if (d.moved === filterMovement) {
+ entry.classed('highlighted', true);
+ entry.classed('dimmed', false);
+ // Set stroke to ring color with enhanced thickness
+ shape.attr('stroke', ring.color);
+ shape.attr('stroke-width', 4);
+ } else {
+ entry.classed('highlighted', false);
+ entry.classed('dimmed', true);
+ // Reset to normal stroke
+ shape.attr('stroke', '#fff');
+ shape.attr('stroke-width', 2);
+ }
+ });
+ }
+ }
+
// Generate technology lists by quadrant
function generateTechLists() {
const techListsContainer = document.getElementById('tech-lists');
@@ -649,6 +757,7 @@
{{ title }}
// Initial render
renderRadar();
generateTechLists();
+ setupLegendFiltering();
console.log("Tech Radar initialized with", config.entries.length, "entries");
})();
From 8d23631600f23a8a7fafe22297aef9458c984678 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:03:22 +0100
Subject: [PATCH 12/21] feat: enhance tooltip functionality and improve styling
in radar visualization
---
docs/tech-radar.md | 2 +-
mkdocs.yml | 38 ++---
mkdocs_tech_radar/templates/radar_inline.html | 135 +++++++++++-------
pyproject.toml | 4 +
4 files changed, 112 insertions(+), 67 deletions(-)
diff --git a/docs/tech-radar.md b/docs/tech-radar.md
index 66e899e..df540c0 100644
--- a/docs/tech-radar.md
+++ b/docs/tech-radar.md
@@ -1,6 +1,6 @@
# Tech Radar
-Our technology radar helps us track the technologies we use and evaluate emerging trends in our development ecosystem.
+The technology radar helps us track the technologies we use and evaluate emerging trends in our development ecosystem.
diff --git a/mkdocs.yml b/mkdocs.yml
index 897dd1d..c8c9e03 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,27 +1,31 @@
site_name: Tech Radar Test
site_description: Testing the tech-radar plugin
+site_url: "https://thatmlopsguy.github.io/mkdocs-tech-radar/"
+repo_url: "https://github.com/thatmlopsguy/mkdocs-tech-radar"
+repo_name: "mkdocs-tech-radar"
+copyright: Copyright © 2025 That MLops Guy
nav:
- Home: index.md
- Tech Radar: tech-radar.md
-theme:
- name: "material"
- palette:
- - media: "(prefers-color-scheme: light)"
- scheme: default
- primary: black
- accent: amber
- toggle:
- icon: material/toggle-switch
- name: Switch to light mode
- - media: "(prefers-color-scheme: dark)"
- scheme: slate
- primary: black
- accent: amber
- toggle:
- icon: material/toggle-switch-off-outline
- name: Switch to dark mode
+# theme:
+# name: "material"
+# palette:
+# - media: "(prefers-color-scheme: light)"
+# scheme: default
+# primary: black
+# accent: amber
+# toggle:
+# icon: material/toggle-switch
+# name: Switch to light mode
+# - media: "(prefers-color-scheme: dark)"
+# scheme: slate
+# primary: black
+# accent: amber
+# toggle:
+# icon: material/toggle-switch-off-outline
+# name: Switch to dark mode
plugins:
- search
diff --git a/mkdocs_tech_radar/templates/radar_inline.html b/mkdocs_tech_radar/templates/radar_inline.html
index 0900e86..109c78f 100644
--- a/mkdocs_tech_radar/templates/radar_inline.html
+++ b/mkdocs_tech_radar/templates/radar_inline.html
@@ -39,6 +39,12 @@
margin: 0 auto;
}
+ /* Ensure tooltip container for default theme compatibility */
+ #tech-radar {
+ position: relative;
+ isolation: isolate;
+ }
+
.radar-svg {
width: 100%;
height: auto;
@@ -140,16 +146,65 @@
.tooltip {
position: absolute;
- background: var(--tooltip-bg);
- color: var(--tooltip-text);
+ background: #000;
+ color: #fff;
padding: 8px 12px;
border-radius: 4px;
font-family: Arial, sans-serif;
font-size: 12px;
pointer-events: none;
- z-index: 1000;
+ z-index: 999999;
+ max-width: 250px;
+ word-wrap: break-word;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ border: 1px solid #333;
+ display: none;
+ white-space: nowrap;
+ }
+
+ /* Tooltip styles */
+ .radar-tooltip {
+ position: absolute;
+ background: rgba(0, 0, 0, 0.9);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 1.4;
+ pointer-events: none;
+ z-index: 10000;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
+ max-width: 300px;
+ word-wrap: break-word;
+ white-space: normal;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+ }
+
+ .radar-tooltip.visible {
+ opacity: 1;
+ }
+
+ /* CSS-only tooltip fallback */
+ .entry {
+ position: relative;
+ }
+
+ .entry:hover::after {
+ content: attr(data-tooltip);
+ position: fixed;
+ background: rgba(0, 0, 0, 0.9);
+ color: white;
+ padding: 8px 12px;
+ border-radius: 4px;
+ font-family: Arial, sans-serif;
+ font-size: 12px;
+ z-index: 999999;
max-width: 250px;
word-wrap: break-word;
+ white-space: pre-line;
+ pointer-events: none;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.radar-title {
@@ -275,7 +330,8 @@
{{ title }}
-
+
+
▼
@@ -370,7 +426,6 @@
{{ title }}
// Select the SVG element
const svg = d3.select("#tech-radar .radar-svg");
- const tooltip = d3.select("#tooltip");
// Function to render the radar
function renderRadar() {
@@ -549,7 +604,32 @@ {{ title }}
};
// Create the shape for this entry
- createShape(d.moved, pos.x, pos.y, ring.color);
+ const shape = createShape(d.moved, pos.x, pos.y, ring.color);
+
+ // Add tooltip functionality using custom div
+ const ringInfo = config.rings[d.ring];
+ const quadrant = config.quadrants[d.quadrant];
+ const titleText = `${d.label} ${quadrant.name} - ${ringInfo.name}${d.description ? ' ' + d.description : ''}`;
+
+ // Get tooltip element
+ const tooltip = document.getElementById('radar-tooltip');
+
+ // Add mouse event handlers
+ shape.on('mouseenter', function(event) {
+ tooltip.innerHTML = titleText;
+ tooltip.classList.add('visible');
+
+ // Position tooltip near cursor
+ const rect = svg.node().getBoundingClientRect();
+ const mouseX = event.clientX - rect.left;
+ const mouseY = event.clientY - rect.top;
+
+ tooltip.style.left = (mouseX + 10) + 'px';
+ tooltip.style.top = (mouseY - 40) + 'px';
+ })
+ .on('mouseleave', function() {
+ tooltip.classList.remove('visible');
+ });
// Add number label on top of the shape
group.append("text")
@@ -561,49 +641,6 @@ {{ title }}
.attr("font-weight", "bold")
.attr("text-anchor", "middle")
.attr("pointer-events", "none");
-
- // Hover effects
- group
- .on("mouseover", function(event, d) {
- const ring = config.rings[d.ring];
- const quadrant = config.quadrants[d.quadrant];
- const entryPos = d.calculatedPos;
-
- // Get the container position to calculate relative positioning
- const containerRect = document.querySelector('.radar-container').getBoundingClientRect();
- const svgRect = document.querySelector('.radar-svg').getBoundingClientRect();
-
- // Calculate tooltip position relative to the point
- const tooltipX = svgRect.left + entryPos.x * (svgRect.width / width) - containerRect.left;
- const tooltipY = svgRect.top + entryPos.y * (svgRect.height / height) - containerRect.top;
-
- tooltip
- .style("display", "block")
- .html(`
- ${d.label}
- ${quadrant.name} - ${ring.name}
- ${d.description || ''}
- ${d.movement ? `${d.movement} ` : ''}
- `)
- .style("left", (tooltipX + 15) + "px")
- .style("top", (tooltipY - 35) + "px");
- })
- .on("mousemove", function(event, d) {
- // Keep tooltip position fixed to the point, don't follow mouse
- const containerRect = document.querySelector('.radar-container').getBoundingClientRect();
- const svgRect = document.querySelector('.radar-svg').getBoundingClientRect();
- const entryPos = d.calculatedPos;
-
- const tooltipX = svgRect.left + entryPos.x * (svgRect.width / width) - containerRect.left;
- const tooltipY = svgRect.top + entryPos.y * (svgRect.height / height) - containerRect.top;
-
- tooltip
- .style("left", (tooltipX + 15) + "px")
- .style("top", (tooltipY - 35) + "px");
- })
- .on("mouseout", function() {
- tooltip.style("display", "none");
- });
});
console.log("Tech Radar rendered with", config.entries.length, "entries");
diff --git a/pyproject.toml b/pyproject.toml
index e23d5b2..b850187 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,6 +7,7 @@ authors = [
]
readme = "README.md"
license = "Apache-2.0"
+keywords = ["mkdocs", "mkdocs-plugin", "documentation", "tech-radar"]
requires-python = ">=3.10"
dependencies = [
"mkdocs>=1.4.0",
@@ -29,6 +30,9 @@ build-backend = "setuptools.build_meta"
include = ["mkdocs_tech_radar*"]
exclude = ["site*", "tests*"]
+[tool.setuptools.package-data]
+mkdocs_tech_radar = ["templates/*"]
+
[dependency-groups]
dev = [
"commitizen>=4.8.3",
From 6dfce8b8ce0e9a24621cb73b06835f7aea95507d Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:15:28 +0100
Subject: [PATCH 13/21] feat: add Makefile for project management and enhance
dev dependencies in pyproject.toml
---
Makefile | 110 +++++++++++++++++++++++++++++++++++++++++++++++++
pyproject.toml | 2 +
2 files changed, 112 insertions(+)
create mode 100644 Makefile
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8b5ee20
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,110 @@
+.PHONY: help install dev test clean build serve lint format check docs
+
+##@ General
+help: ## Show this help
+ @awk 'BEGIN {FS = ":.*##"; printf "\nUsage: \033[36m\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-26s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
+
+##@ Environment setup
+install: ## Install dependencies using uv
+ uv sync
+
+dev: ## Install development dependencies
+ uv sync --dev
+
+##@ Development
+serve: ## Start the MkDocs development server
+ uv run mkdocs serve
+
+serve-custom: ## Start the MkDocs development server on custom port
+ uv run mkdocs serve --dev-addr 127.0.0.1:8001
+
+build: ## Build the MkDocs site
+ uv run mkdocs build
+
+##@ Package management
+install-pkg: ## Install the package in editable mode
+ uv pip install --editable .
+
+reinstall: ## Uninstall and reinstall the package
+ uv pip uninstall mkdocs-tech-radar -q && uv pip install --editable . -q
+
+##@ Testing
+test: ## Run tests (installs dev dependencies if needed)
+ @uv sync --dev --quiet
+ uv run pytest
+
+test-verbose: ## Run tests with verbose output
+ @uv sync --dev --quiet
+ uv run pytest -v
+
+test-coverage: ## Run tests with coverage report
+ @uv sync --dev --quiet
+ uv run pytest --cov=mkdocs_tech_radar --cov-report=html --cov-report=term
+
+##@ Code quality
+lint: ## Run linting with ruff
+ uv run ruff check .
+
+lint-fix: ## Run linting and fix auto-fixable issues
+ uv run ruff check . --fix
+
+format: ## Format code with ruff
+ uv run ruff format .
+
+format-check: ## Check code formatting without making changes
+ uv run ruff format . --check
+
+check: ## Run all quality checks (lint + format check + test)
+ @uv sync --dev --quiet
+ uv run ruff check .
+ uv run ruff format . --check
+ uv run pytest
+
+##@ Documentation
+docs-build: ## Build documentation
+ uv run mkdocs build
+
+docs-deploy: ## Deploy documentation to GitHub Pages
+ uv run mkdocs gh-deploy
+
+##@ Cleanup
+clean: ## Remove build artifacts and cache
+ rm -rf build/
+ rm -rf dist/
+ rm -rf site/
+ rm -rf *.egg-info/
+ find . -type d -name __pycache__ -exec rm -rf {} +
+ find . -type f -name "*.pyc" -delete
+
+clean-venv: ## Remove virtual environment
+ rm -rf .venv
+
+##@ Package building and publishing
+build-package: ## Build the package
+ uv build
+
+publish-test: ## Publish to TestPyPI
+ uv publish --repository testpypi
+
+publish: ## Publish to PyPI
+ uv publish
+
+##@ Dependency management
+update: ## Update dependencies
+ uv sync --upgrade
+
+lock: ## Update lock file
+ uv lock
+
+add: ## Add a new dependency (use: make add PKG=package_name)
+ uv add $(PKG)
+
+add-dev: ## Add a new development dependency (use: make add-dev PKG=package_name)
+ uv add --dev $(PKG)
+
+remove: ## Remove a dependency (use: make remove PKG=package_name)
+ uv remove $(PKG)
+
+##@ Utility
+show: ## Show installed packages
+ uv pip list
diff --git a/pyproject.toml b/pyproject.toml
index b850187..463e503 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,8 @@ mkdocs_tech_radar = ["templates/*"]
dev = [
"commitizen>=4.8.3",
"prek>=0.1.1",
+ "pytest>=8.4.1",
+ "pytest-cov>=6.2.1",
"ruff>=0.12.9",
]
From 6b6dff39527eabd66c9d23b13428300b449b7419 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:27:51 +0100
Subject: [PATCH 14/21] docs: streamline README by removing redundant features
section and enhancing clarity on radar file structure
---
README.md | 95 ++++++++++++++-----------------------------------------
1 file changed, 23 insertions(+), 72 deletions(-)
diff --git a/README.md b/README.md
index f86bd73..eec16f5 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,14 @@
# MkDocs Tech Radar Plugin
A MkDocs plugin that generates an interactive Technology Radar based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
-This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions across multiple radar versions.
-## Features
+This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions.
-- 🎯 **Interactive visualization** with D3.js-powered radar chart
-- 📊 **Four-ring assessment model** (Adopt, Trial, Assess, Hold)
-- 🏗️ **Four-quadrant organization** (Frameworks, Tools, Platforms, Techniques)
-- 🎭 **Movement indicators** (moved up ▲, moved down ▼, new ★, no change ⬤)
-- 💡 **Hover tooltips** and interactive legend
-- ️🎨 **Multiple radar versions** - supports a folder of radar files with dropdown selector
-- 🌙 **Dark mode support** - automatically adapts to MkDocs Material theme
-- 📋 **Inline embedding** - radar is embedded directly in your documentation pages
+See [live demo](https://thatmlopsguy.github.io/mkdocs-tech-radar)
## Installation
-Install the plugin using uv, pip, or your preferred Python package manager:
+Install the plugin using `uv`, `pip`, or your preferred Python package manager:
```bash
# Using uv
@@ -30,6 +22,16 @@ pip install mkdocs-tech-radar
Add the plugin to your `mkdocs.yml`:
+### Parameters
+
+| Option | Type | Default | Description |
+|-----------------|--------|----------------------|------------------------------------|
+| `title_name` | string | `'Technology Radar'` | Title for the radar |
+| `target_page` | string | `'tech-radar.md'` | Page to embed the radar in |
+| `radars_folder` | string | `'radars'` | Folder containing radar JSON files |
+
+### Example:
+
```yaml
plugins:
- tech-radar:
@@ -40,9 +42,7 @@ plugins:
## Usage
-### Creating Multiple Radar Files
-
-Create multiple JSON files in a `docs/radars/` folder for different radar versions:
+Create one or multiple JSON files in a `docs/radars/` folder for different radar versions:
```text
docs/
@@ -52,43 +52,18 @@ docs/
2025-09-01.json
```
+**Note:** Use date-based naming for optimal sorting:
+
+- `2025-08-20.json` - August 20, 2025 version
+- `2025-09-01.json` - September 1, 2025 version
+- `2025-12-15.json` - December 15, 2025 version
+
The plugin will:
- Automatically load all JSON files from the `radars_folder`
- Display a dropdown selector for switching between radar versions
- Default to the newest version (based on filename sorting)
-Example radar file (`docs/radars/2025-08-20.json`):
-
-```json
-{
- "date": "2025-08-20",
- "quadrants": [
- {"name": "Languages & Frameworks"},
- {"name": "Tools"},
- {"name": "Platforms"},
- {"name": "Techniques"}
- ],
- "rings": [
- {"name": "ADOPT", "color": "#5ba300", "description": "Technologies we have high confidence in"},
- {"name": "TRIAL", "color": "#009eb0", "description": "Worth pursuing with small risks"},
- {"name": "ASSESS", "color": "#c7ba00", "description": "Worth exploring with higher risks"},
- {"name": "HOLD", "color": "#e09b96", "description": "Proceed with caution or avoid"}
- ],
- "entries": [
- {
- "label": "React",
- "quadrant": 0,
- "ring": 0,
- "moved": 0,
- "active": true,
- "description": "A JavaScript library for building user interfaces",
- "link": "https://reactjs.org/"
- }
- ]
-}
-```
-
### Adding Radar to Your Documentation
Add the radar placeholder in your target markdown page:
@@ -105,39 +80,13 @@ Additional content can go here after the radar.
The `` placeholder will be replaced with the interactive radar visualization.
-### Multiple Radar Versions
-
-The plugin provides these features for managing multiple radar versions:
-
-#### Version Selection
-
-- **Dropdown menu**: Appears automatically when multiple radar files are detected
-- **Date-based sorting**: Files are sorted by filename (assuming YYYY-MM-DD format)
-- **Auto-selection**: Newest version is selected by default
-- **Dynamic switching**: Users can switch between radar versions without page reload
-
-#### File Naming Convention
-
-Use date-based naming for optimal sorting:
-
-- `2025-08-20.json` - August 20, 2025 version
-- `2025-09-01.json` - September 1, 2025 version
-- `2025-12-15.json` - December 15, 2025 version
-
#### Recommended Workflow
1. Create a new radar file for each release/quarter
2. Copy the previous version as a starting point
3. Update entries with new technologies and movement indicators
4. The plugin automatically detects and makes it available
-
-## Configuration Options
-
-| Option | Type | Default | Description |
-|-----------------|--------|----------------------|------------------------------------|
-| `title_name` | string | `'Technology Radar'` | Title for the radar |
-| `target_page` | string | `'tech-radar.md'` | Page to embed the radar in |
-| `radars_folder` | string | `'radars'` | Folder containing radar JSON files |
+5. Commit the changes to your version control system
## Radar File Structure
@@ -176,6 +125,8 @@ Each entry in the `entries` array must have:
### Example Complete Radar File
+Example radar file (`docs/radars/2025-08-20.json`):
+
```json
{
"date": "2025-08-20",
From 0ff374f0a0f8d1eb912f0cad2a5b7accbe1ff8c2 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:37:26 +0100
Subject: [PATCH 15/21] feat: add GitHub Actions workflow for documentation
build and deployment; update Python version requirement in pyproject.toml
---
.github/workflows/docs.yml | 80 ++++++++++++++++++++++++++++++++++++++
.vscode/settings.json | 5 +++
pyproject.toml | 2 +-
3 files changed, 86 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/docs.yml
create mode 100644 .vscode/settings.json
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..e86eb85
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,80 @@
+name: docs
+
+on:
+ push:
+ branches:
+ - main
+ - dev
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ - 'mkdocs_tech_radar/**'
+ - 'pyproject.toml'
+
+ pull_request:
+ branches:
+ - main
+ paths:
+ - 'docs/**'
+ - 'mkdocs.yml'
+ - 'mkdocs_tech_radar/**'
+ - 'pyproject.toml'
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
+# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+concurrency:
+ group: "pages"
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ with:
+ python-version: 3.12
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v5
+ with:
+ version: "latest"
+
+ - name: Install dependencies and plugin
+ run: |
+ uv pip install --system --editable .
+
+ - name: Setup Pages
+ id: pages
+ uses: actions/configure-pages@v5
+
+ - name: Build documentation
+ run: |
+ uv run mkdocs build --config-file mkdocs.yml
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./site
+
+ deploy:
+ if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v5
+
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..5ca369d
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "cSpell.words": [
+ "pyproject"
+ ]
+}
diff --git a/pyproject.toml b/pyproject.toml
index 463e503..13af7d3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -8,7 +8,7 @@ authors = [
readme = "README.md"
license = "Apache-2.0"
keywords = ["mkdocs", "mkdocs-plugin", "documentation", "tech-radar"]
-requires-python = ">=3.10"
+requires-python = ">=3.12"
dependencies = [
"mkdocs>=1.4.0",
"jinja2>=3.0.0",
From 8f8cb6c19b5331385b9b47c15e3f60a6b5583923 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:43:31 +0100
Subject: [PATCH 16/21] fix: update deploy action version to v4 in GitHub
Actions workflow; add documentation badge to README
---
.github/workflows/docs.yml | 2 +-
README.md | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index e86eb85..43bc5f0 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -75,6 +75,6 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v5
+ uses: actions/deploy-pages@v4
diff --git a/README.md b/README.md
index eec16f5..a3af4cd 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# MkDocs Tech Radar Plugin
+[](https://github.com/thatmlopsguy/mkdocs-tech-radar/actions/workflows/docs.yml)
+
A MkDocs plugin that generates an interactive Technology Radar based on the [Zalando Tech Radar](https://github.com/zalando/tech-radar) implementation.
This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions.
From 45a61f5ec78109d2c2a91146f73ff20c331461d1 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:46:10 +0100
Subject: [PATCH 17/21] feat: enable manual triggering of documentation
workflow with workflow_dispatch
---
.github/workflows/docs.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 43bc5f0..3c520db 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,6 +1,7 @@
name: docs
on:
+ workflow_dispatch:
push:
branches:
- main
From 1831df012cb2f25ce759e0d28a99f33987733191 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:47:44 +0100
Subject: [PATCH 18/21] fix: comment out conditional deployment for dev branch
in GitHub Actions workflow
---
.github/workflows/docs.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 3c520db..ff1ee59 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -67,7 +67,7 @@ jobs:
path: ./site
deploy:
- if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
+ # if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
From fb4dee7eda5069d6c6591ce286622cdfe1ec379b Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 16:57:22 +0100
Subject: [PATCH 19/21] fix: update concurrency settings to cancel in-progress
deployments and remove conditional deployment for dev branch
---
.github/workflows/docs.yml | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index ff1ee59..9c23e25 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -27,10 +27,10 @@ permissions:
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
-# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
+# Cancel in-progress deployments when a new one is triggered to prevent stuck deployments.
concurrency:
group: "pages"
- cancel-in-progress: false
+ cancel-in-progress: true
jobs:
build:
@@ -67,7 +67,6 @@ jobs:
path: ./site
deploy:
- # if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
From e8dddf46d358466ec6cf80aa654ed16a5708cd43 Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 17:01:55 +0100
Subject: [PATCH 20/21] fix: update site name and description in mkdocs
configuration; add Tech Radar navigation instruction in index
---
docs/index.md | 2 ++
mkdocs.yml | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/docs/index.md b/docs/index.md
index abfc817..d671431 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -7,3 +7,5 @@ This will not work, because the mkdocs config only check in the `target_page`.
-->
+
+Go to the Tech Radar tab to see the interactive technology radar.
diff --git a/mkdocs.yml b/mkdocs.yml
index c8c9e03..c58b8e2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,5 +1,5 @@
-site_name: Tech Radar Test
-site_description: Testing the tech-radar plugin
+site_name: Tech Radar Live Demo
+site_description: Demo of the tech-radar mkdocs plugin
site_url: "https://thatmlopsguy.github.io/mkdocs-tech-radar/"
repo_url: "https://github.com/thatmlopsguy/mkdocs-tech-radar"
repo_name: "mkdocs-tech-radar"
From 0bdf663da0283cae95bfb20f3663decc99ecb45f Mon Sep 17 00:00:00 2001
From: thatmlopsguy <165834479+thatmlopsguy@users.noreply.github.com>
Date: Thu, 21 Aug 2025 17:11:48 +0100
Subject: [PATCH 21/21] fix: update GitHub Actions workflow to remove dev
branch from push triggers; update action versions and improve README
formatting
---
.github/workflows/docs.yml | 13 ++++---------
README.md | 8 +++-----
2 files changed, 7 insertions(+), 14 deletions(-)
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 9c23e25..b9c924e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,11 +1,9 @@
name: docs
on:
- workflow_dispatch:
push:
branches:
- main
- - dev
paths:
- 'docs/**'
- 'mkdocs.yml'
@@ -40,12 +38,8 @@ jobs:
with:
fetch-depth: 0
- - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- with:
- python-version: 3.12
-
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
with:
version: "latest"
@@ -55,18 +49,19 @@ jobs:
- name: Setup Pages
id: pages
- uses: actions/configure-pages@v5
+ uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0
- name: Build documentation
run: |
uv run mkdocs build --config-file mkdocs.yml
- name: Upload artifact
- uses: actions/upload-pages-artifact@v3
+ uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
with:
path: ./site
deploy:
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
diff --git a/README.md b/README.md
index a3af4cd..7886f01 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ A MkDocs plugin that generates an interactive Technology Radar based on the [Zal
This plugin helps engineering teams visualize and track their technology adoption, assessment, and decisions.
-See [live demo](https://thatmlopsguy.github.io/mkdocs-tech-radar)
+**See [live demo](https://thatmlopsguy.github.io/mkdocs-tech-radar)**
## Installation
@@ -32,7 +32,7 @@ Add the plugin to your `mkdocs.yml`:
| `target_page` | string | `'tech-radar.md'` | Page to embed the radar in |
| `radars_folder` | string | `'radars'` | Folder containing radar JSON files |
-### Example:
+### Example
```yaml
plugins:
@@ -73,7 +73,7 @@ Add the radar placeholder in your target markdown page:
```markdown
# Tech Radar
-Our technology radar helps us track the technologies we use and evaluate emerging trends.
+The technology radar helps us track the technologies we use and evaluate emerging trends.
@@ -178,8 +178,6 @@ You can customize the radar appearance by modifying the radar files. The radar u
You can change these colors by modifying the `rings` array in your radar JSON files.
-## Example
-
Check out the included example in the `docs/` directory:
1. Build the documentation: `uv run mkdocs build`