feat: render blips on radar

This commit is contained in:
Mathias Schopmans
2024-02-21 10:37:25 +01:00
committed by Mathias Schopmans
parent 1b7634a2ef
commit 16f29cd4d4
9 changed files with 128 additions and 13 deletions

View File

@@ -36,22 +36,22 @@
"description": "We can clearly recommend this technology. We have used it for longer period of time in many teams and it has proven to be stable and useful.",
"color": "#5cb449",
"radius": 0.5,
"strokeWidth": 6
"strokeWidth": 5
},
{
"id": "trial",
"title": "Trial",
"description": "We have used it with success and recommend to have a closer look at the technology in this ring. The goal of items here is to look at them more closely, with the goal to bring them to the adopt level.",
"color": "#faa03d",
"radius": 0.7,
"strokeWidth": 4
"radius": 0.69,
"strokeWidth": 3
},
{
"id": "assess",
"title": "Assess",
"description": "We have tried it out and we find it promising. We recommend having a look at these items when you face a specific need for the technology in your project.",
"color": "#029df7",
"radius": 0.88,
"radius": 0.85,
"strokeWidth": 2
},
{
@@ -60,7 +60,7 @@
"description": "This category is a bit special. Unlike the others, we recommend to stop doing or using something. That does not mean that they are bad and it often might be ok to use them in existing projects. But we move things here if we think we shouldn't do them anymore - because we see better options or alternatives now.",
"color": "#688190",
"radius": 1,
"strokeWidth": 1
"strokeWidth": 0.75
}
],
"flags": {
@@ -74,5 +74,9 @@
"title": "Changed",
"titleShort": "C"
}
},
"chart": {
"size": 800,
"blipSize": 12
}
}

View File

@@ -1,7 +1,8 @@
/** @type {import('next').NextConfig} */
/** @type {import("next").NextConfig} */
const nextConfig = {
output: "export",
// basePath: '/techradar',
trailingSlash: true,
reactStrictMode: true,
};

View File

@@ -144,6 +144,8 @@ function postProcessItems(items: Item[]): {
const processedItems = items.map((item) => ({
...item,
// @todo: Maybe we should use a better random number generator to avoid overlapping of blips
random: [Math.sqrt(Math.random()), Math.random()] as [number, number],
flag: getFlag(item, latestRelease),
// only keep revision which ring or body is different
revisions: item.revisions?.filter((revision, index, revisions) => {

View File

@@ -0,0 +1,58 @@
import React from "react";
import { getChartConfig } from "@/lib/data";
import { Flag } from "@/lib/types";
const { blipSize } = getChartConfig();
const halfBlipSize = blipSize / 2;
interface BlipProps {
color: string;
x: number;
y: number;
}
export function Blip({ flag, color, x, y }: BlipProps & { flag: Flag }) {
switch (flag) {
case Flag.New:
return <BlipNew x={x} y={y} color={color} />;
case Flag.Changed:
return <BlipChanged x={x} y={y} color={color} />;
default:
return <BlipDefault x={x} y={y} color={color} />;
}
}
function BlipNew({ x, y, color }: BlipProps) {
x = Math.round(x - halfBlipSize);
y = Math.round(y - halfBlipSize);
return (
<path
stroke="none"
fill={color}
d="M5.7679491924311 2.1387840678323a2 2 0 0 1 3.4641016151378 0l5.0358983848622 8.7224318643355a2 2 0 0 1 -1.7320508075689 3l-10.071796769724 0a2 2 0 0 1 -1.7320508075689 -3"
transform={`translate(${x},${y})`}
/>
);
}
function BlipChanged({ x, y, color }: BlipProps) {
x = Math.round(x - halfBlipSize);
y = Math.round(y - halfBlipSize);
return (
<rect
transform={`rotate(-45 ${x} ${y})`}
x={x}
y={y}
width={blipSize}
height={blipSize}
rx="3"
stroke="none"
fill={color}
/>
);
}
function BlipDefault({ x, y, color }: BlipProps) {
return <circle cx={x} cy={y} r={halfBlipSize} stroke="none" fill={color} />;
}

View File

@@ -1,5 +1,6 @@
.radar {
padding: 50px;
padding: 0 15px;
margin-bottom: 60px;
position: relative;
}

View File

@@ -1,16 +1,24 @@
import Link from "next/link";
import React, { FC } from "react";
import styles from "./Radar.module.css";
import { Quadrant, Ring } from "@/lib/types";
import { Blip } from "@/components/Radar/Blip";
import { Item, Quadrant, Ring } from "@/lib/types";
export interface RadarProps {
size?: number;
quadrants: Quadrant[];
rings: Ring[];
items: Item[];
}
export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
export const Radar: FC<RadarProps> = ({
size = 800,
quadrants = [],
rings = [],
items = [],
}) => {
const viewBoxSize = size;
const center = size / 2;
const startAngles = [270, 0, 180, 90]; // Corresponding to positions 1, 2, 3, and 4 respectively
@@ -22,8 +30,8 @@ export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
): { x: number; y: number } => {
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
return {
x: center + radius * Math.cos(angleInRadians),
y: center + radius * Math.sin(angleInRadians),
x: Math.round(center + radius * Math.cos(angleInRadians)),
y: Math.round(center + radius * Math.sin(angleInRadians)),
};
};
@@ -71,6 +79,41 @@ export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
);
};
// Function to place items inside their rings and quadrants
const renderItem = (item: Item) => {
const ring = rings.find((r) => r.id === item.ring);
const quadrant = quadrants.find((q) => q.id === item.quadrant);
if (!ring || !quadrant) return null; // If no ring or quadrant, don't render item
const padding = 15; // Padding in pixels
const paddingAngle = 10; // Padding in degrees
// Random factors to determine position within the ring
const [randomRadius, randomAngleFactor] = item.random || [
Math.sqrt(Math.random()),
Math.random(),
];
const innerRadius =
(rings[rings.indexOf(ring) - 1]?.radius || 0) + padding / center; // Add inner padding
const outerRadius = (ring.radius || 1) - padding / center; // Subtract outer padding
const ringWidth = (outerRadius - innerRadius) * center; // Width of the ring in the SVG
// Calculate the position within the ring
const itemRadius = innerRadius * center + randomRadius * ringWidth;
// Calculate the angle with padding offset, avoiding the exact edges
const startAngle = startAngles[quadrant.position - 1] + paddingAngle;
const endAngle = startAngle + 90 - 2 * paddingAngle; // Subtract padding from both sides
const itemAngle = startAngle + (endAngle - startAngle) * randomAngleFactor;
// Convert polar coordinates to cartesian for the item's position
const { x, y } = polarToCartesian(itemRadius, itemAngle);
return (
<Link key={item.id} href={`/${item.quadrant}/${item.id}`}>
<Blip flag={item.flag} color={quadrant.color} x={x} y={y} />
</Link>
);
};
return (
<div className={styles.radar}>
<svg
@@ -80,7 +123,7 @@ export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
>
{quadrants.map((quadrant) => (
<g className={`quadrant quadrant-${quadrant.id}`} key={quadrant.id}>
<g key={quadrant.id} data-quadrant={quadrant.id}>
{renderGlow(quadrant.position, quadrant.color)}
{rings.map((ring) => (
<path
@@ -94,6 +137,7 @@ export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
))}
</g>
))}
<g>{items.map((item) => renderItem(item))}</g>
</svg>
</div>
);

View File

@@ -12,6 +12,10 @@ export function getAppName() {
return messages.radarName;
}
export function getChartConfig() {
return config.chart;
}
export function getFlag(flag: Exclude<Flag, Flag.Default>) {
return config.flags[flag];
}

View File

@@ -23,6 +23,7 @@ export interface Item {
flag: Flag;
tags: string[];
revisions?: Revision[];
random?: [radius: number, angle: number];
}
export interface Ring {

View File

@@ -21,7 +21,7 @@ const Home: CustomPage = () => {
{appName}{" "}
<span style={{ color: "var(--highlight)" }}>Version #{version}</span>
</h1>
<Radar quadrants={quadrants} rings={rings} />
<Radar quadrants={quadrants} rings={rings} items={items} />
<QuadrantList items={items} />
</>
);