diff --git a/README.md b/README.md
index 0cb35c1c..410dc606 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ Feel free to use and adapt it for your own purposes.
```html
-
+
```
2. insert an empty `svg` tag:
@@ -31,6 +31,7 @@ radar_visualization({
svg_id: "radar",
width: 1450,
height: 1000,
+ scale: 1.0,
colors: {
background: "#fff",
grid: "#bbb",
@@ -65,7 +66,8 @@ radar_visualization({
});
```
-Entries are positioned automatically so that they don't overlap.
+Entries are positioned automatically so that they don't overlap. The "scale" parameter can help
+in adjusting the size of the radar.
As a working example, you can check out `docs/index.html` — the source of our [public Tech
Radar](http://zalando.github.io/tech-radar/).
@@ -99,7 +101,7 @@ http://localhost:3000/
```
The MIT License (MIT)
-Copyright (c) 2017-2022 Zalando SE
+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
diff --git a/docs/radar.js b/docs/radar.js
index 1fbda6f8..f157ad3d 100644
--- a/docs/radar.js
+++ b/docs/radar.js
@@ -1,6 +1,6 @@
// The MIT License (MIT)
-// Copyright (c) 2017 Zalando SE
+// 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
diff --git a/docs/release/radar-0.8.js b/docs/release/radar-0.8.js
new file mode 100644
index 00000000..f157ad3d
--- /dev/null
+++ b/docs/release/radar-0.8.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);
+}