diff --git a/containers/techradar/zalando/README.md b/containers/techradar/zalando/README.md
new file mode 100644
index 0000000..95bfcaa
--- /dev/null
+++ b/containers/techradar/zalando/README.md
@@ -0,0 +1,13 @@
+# Tech radar
+
+It **is** [Zalando's tech radar], just customized.
+
+Serves a static page.
+
+There are **4** quadrants.
+Quadrants start from the **bottom right** and go clockwise: `0` is _bottom right_, `1` is _bottom left_, `2` is
+_top left_, `3` is _top right_.
+
+Moved: `-1` is _down_, `0` is _stationary_, `1` is _up_.
+
+[zalando's tech radar]: https://github.com/zalando/tech-radar
diff --git a/containers/techradar/zalando/docker-compose.yml b/containers/techradar/zalando/docker-compose.yml
new file mode 100644
index 0000000..f7ee735
--- /dev/null
+++ b/containers/techradar/zalando/docker-compose.yml
@@ -0,0 +1,13 @@
+---
+version: '3'
+services:
+ radar:
+ container_name: radar
+ image: nginxinc/nginx-unprivileged:1.25.5-bookworm-perl
+ volumes:
+ - ${PWD}:/usr/share/nginx/html:ro
+ ports:
+ - 8080:8080
+ environment:
+ NGINX_HOST: localhost
+ NGINX_PORT: '8080'
diff --git a/containers/techradar/zalando/index.html b/containers/techradar/zalando/index.html
new file mode 100644
index 0000000..f3e3586
--- /dev/null
+++ b/containers/techradar/zalando/index.html
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ Tech Radar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/containers/techradar/zalando/radar.css b/containers/techradar/zalando/radar.css
new file mode 100644
index 0000000..662f808
--- /dev/null
+++ b/containers/techradar/zalando/radar.css
@@ -0,0 +1,23 @@
+body {
+ font-family: 'Source Sans Pro', arial, helvetica, sans-serif;
+ padding-bottom: 50px;
+}
+
+h3 {
+ margin-top: 50px;
+}
+
+li {
+ margin: 25px 50px 0 0;
+}
+
+table {
+ width: 1400px;
+ margin: 0 50px 0 50px;
+}
+
+td {
+ width: 50%;
+ vertical-align: top;
+ padding-right: 60px;
+}
diff --git a/containers/techradar/zalando/radar.js b/containers/techradar/zalando/radar.js
new file mode 100644
index 0000000..72879c6
--- /dev/null
+++ b/containers/techradar/zalando/radar.js
@@ -0,0 +1,474 @@
+// The MIT License (MIT)
+
+// Copyright (c) 2017-2024 Zalando SE
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+
+function radar_visualization(config) {
+
+ // custom random number generator, to make random sequence reproducible
+ // source: https://stackoverflow.com/questions/521295
+ var seed = 42;
+ function random() {
+ var x = Math.sin(seed++) * 10000;
+ return x - Math.floor(x);
+ }
+
+ function random_between(min, max) {
+ return min + random() * (max - min);
+ }
+
+ function normal_between(min, max) {
+ return min + (random() + random()) * 0.5 * (max - min);
+ }
+
+ // radial_min / radial_max are multiples of PI
+ const quadrants = [
+ { radial_min: 0, radial_max: 0.5, factor_x: 1, factor_y: 1 },
+ { radial_min: 0.5, radial_max: 1, factor_x: -1, factor_y: 1 },
+ { radial_min: -1, radial_max: -0.5, factor_x: -1, factor_y: -1 },
+ { radial_min: -0.5, radial_max: 0, factor_x: 1, factor_y: -1 }
+ ];
+
+ const rings = [
+ { radius: 130 },
+ { radius: 220 },
+ { radius: 310 },
+ { radius: 400 }
+ ];
+
+ const title_offset =
+ { x: -675, y: -420 };
+
+ const footer_offset =
+ { x: -675, y: 420 };
+
+ const legend_offset = [
+ { x: 450, y: 90 },
+ { x: -675, y: 90 },
+ { x: -675, y: -310 },
+ { x: 450, y: -310 }
+ ];
+
+ function polar(cartesian) {
+ var x = cartesian.x;
+ var y = cartesian.y;
+ return {
+ t: Math.atan2(y, x),
+ r: Math.sqrt(x * x + y * y)
+ }
+ }
+
+ function cartesian(polar) {
+ return {
+ x: polar.r * Math.cos(polar.t),
+ y: polar.r * Math.sin(polar.t)
+ }
+ }
+
+ function bounded_interval(value, min, max) {
+ var low = Math.min(min, max);
+ var high = Math.max(min, max);
+ return Math.min(Math.max(value, low), high);
+ }
+
+ function bounded_ring(polar, r_min, r_max) {
+ return {
+ t: polar.t,
+ r: bounded_interval(polar.r, r_min, r_max)
+ }
+ }
+
+ function bounded_box(point, min, max) {
+ return {
+ x: bounded_interval(point.x, min.x, max.x),
+ y: bounded_interval(point.y, min.y, max.y)
+ }
+ }
+
+ function segment(quadrant, ring) {
+ var polar_min = {
+ t: quadrants[quadrant].radial_min * Math.PI,
+ r: ring === 0 ? 30 : rings[ring - 1].radius
+ };
+ var polar_max = {
+ t: quadrants[quadrant].radial_max * Math.PI,
+ r: rings[ring].radius
+ };
+ var cartesian_min = {
+ x: 15 * quadrants[quadrant].factor_x,
+ y: 15 * quadrants[quadrant].factor_y
+ };
+ var cartesian_max = {
+ x: rings[3].radius * quadrants[quadrant].factor_x,
+ y: rings[3].radius * quadrants[quadrant].factor_y
+ };
+ return {
+ clipx: function(d) {
+ var c = bounded_box(d, cartesian_min, cartesian_max);
+ var p = bounded_ring(polar(c), polar_min.r + 15, polar_max.r - 15);
+ d.x = cartesian(p).x; // adjust data too!
+ return d.x;
+ },
+ clipy: function(d) {
+ var c = bounded_box(d, cartesian_min, cartesian_max);
+ var p = bounded_ring(polar(c), polar_min.r + 15, polar_max.r - 15);
+ d.y = cartesian(p).y; // adjust data too!
+ return d.y;
+ },
+ random: function() {
+ return cartesian({
+ t: random_between(polar_min.t, polar_max.t),
+ r: normal_between(polar_min.r, polar_max.r)
+ });
+ }
+ }
+ }
+
+ // position each entry randomly in its segment
+ for (var i = 0; i < config.entries.length; i++) {
+ var entry = config.entries[i];
+ entry.segment = segment(entry.quadrant, entry.ring);
+ var point = entry.segment.random();
+ entry.x = point.x;
+ entry.y = point.y;
+ entry.color = entry.active || config.print_layout ?
+ config.rings[entry.ring].color : config.colors.inactive;
+ }
+
+ // partition entries according to segments
+ var segmented = new Array(4);
+ for (var quadrant = 0; quadrant < 4; quadrant++) {
+ segmented[quadrant] = new Array(4);
+ for (var ring = 0; ring < 4; ring++) {
+ segmented[quadrant][ring] = [];
+ }
+ }
+ for (var i=0; i 0) {
+ blip.append("path")
+ .attr("d", "M -11,5 11,5 0,-13 z") // triangle pointing up
+ .style("fill", d.color);
+ } else if (d.moved < 0) {
+ blip.append("path")
+ .attr("d", "M -11,-5 11,-5 0,13 z") // triangle pointing down
+ .style("fill", d.color);
+ } else {
+ blip.append("circle")
+ .attr("r", 9)
+ .attr("fill", d.color);
+ }
+
+ // blip text
+ if (d.active || config.print_layout) {
+ var blip_text = config.print_layout ? d.id : d.label.match(/[a-z]/i);
+ blip.append("text")
+ .text(blip_text)
+ .attr("y", 3)
+ .attr("text-anchor", "middle")
+ .style("fill", "#fff")
+ .style("font-family", "Arial, Helvetica")
+ .style("font-size", function(d) { return blip_text.length > 2 ? "8px" : "9px"; })
+ .style("pointer-events", "none")
+ .style("user-select", "none");
+ }
+ });
+
+ // make sure that blips stay inside their segment
+ function ticked() {
+ blips.attr("transform", function(d) {
+ return translate(d.segment.clipx(d), d.segment.clipy(d));
+ })
+ }
+
+ // distribute blips, while avoiding collisions
+ d3.forceSimulation()
+ .nodes(config.entries)
+ .velocityDecay(0.19) // magic number (found by experimentation)
+ .force("collision", d3.forceCollide().radius(12).strength(0.85))
+ .on("tick", ticked);
+}
diff --git a/containers/techradar/zalando/radar.json b/containers/techradar/zalando/radar.json
new file mode 100644
index 0000000..f293519
--- /dev/null
+++ b/containers/techradar/zalando/radar.json
@@ -0,0 +1,41 @@
+{
+ "date": "2024.04.25",
+ "entries": [
+ {
+ "quadrant": 2,
+ "ring": 0,
+ "label": "node.js",
+ "active": true,
+ "moved": 0
+ },
+ {
+ "quadrant": 3,
+ "ring": 0,
+ "label": "pulumi",
+ "link": "https://www.pulumi.com/",
+ "active": true,
+ "moved": 0
+ },
+ {
+ "quadrant": 2,
+ "ring": 1,
+ "label": "python",
+ "active": true,
+ "moved": 0
+ },
+ {
+ "quadrant": 2,
+ "ring": 2,
+ "label": "golang",
+ "active": true,
+ "moved": -1
+ },
+ {
+ "quadrant": 1,
+ "ring": 3,
+ "label": "gitea",
+ "active": false,
+ "moved": 1
+ }
+ ]
+}
\ No newline at end of file