feat: render blips on radar
This commit is contained in:
committed by
Mathias Schopmans
parent
1b7634a2ef
commit
16f29cd4d4
@@ -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.",
|
"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",
|
"color": "#5cb449",
|
||||||
"radius": 0.5,
|
"radius": 0.5,
|
||||||
"strokeWidth": 6
|
"strokeWidth": 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "trial",
|
"id": "trial",
|
||||||
"title": "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.",
|
"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",
|
"color": "#faa03d",
|
||||||
"radius": 0.7,
|
"radius": 0.69,
|
||||||
"strokeWidth": 4
|
"strokeWidth": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "assess",
|
"id": "assess",
|
||||||
"title": "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.",
|
"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",
|
"color": "#029df7",
|
||||||
"radius": 0.88,
|
"radius": 0.85,
|
||||||
"strokeWidth": 2
|
"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.",
|
"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",
|
"color": "#688190",
|
||||||
"radius": 1,
|
"radius": 1,
|
||||||
"strokeWidth": 1
|
"strokeWidth": 0.75
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"flags": {
|
"flags": {
|
||||||
@@ -74,5 +74,9 @@
|
|||||||
"title": "Changed",
|
"title": "Changed",
|
||||||
"titleShort": "C"
|
"titleShort": "C"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"size": 800,
|
||||||
|
"blipSize": 12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "export",
|
output: "export",
|
||||||
// basePath: '/techradar',
|
// basePath: '/techradar',
|
||||||
|
trailingSlash: true,
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,8 @@ function postProcessItems(items: Item[]): {
|
|||||||
|
|
||||||
const processedItems = items.map((item) => ({
|
const processedItems = items.map((item) => ({
|
||||||
...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),
|
flag: getFlag(item, latestRelease),
|
||||||
// only keep revision which ring or body is different
|
// only keep revision which ring or body is different
|
||||||
revisions: item.revisions?.filter((revision, index, revisions) => {
|
revisions: item.revisions?.filter((revision, index, revisions) => {
|
||||||
|
|||||||
58
src/components/Radar/Blip.tsx
Normal file
58
src/components/Radar/Blip.tsx
Normal 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} />;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
.radar {
|
.radar {
|
||||||
padding: 50px;
|
padding: 0 15px;
|
||||||
|
margin-bottom: 60px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
|
|
||||||
import styles from "./Radar.module.css";
|
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 {
|
export interface RadarProps {
|
||||||
size?: number;
|
size?: number;
|
||||||
quadrants: Quadrant[];
|
quadrants: Quadrant[];
|
||||||
rings: Ring[];
|
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 viewBoxSize = size;
|
||||||
const center = size / 2;
|
const center = size / 2;
|
||||||
const startAngles = [270, 0, 180, 90]; // Corresponding to positions 1, 2, 3, and 4 respectively
|
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 } => {
|
): { x: number; y: number } => {
|
||||||
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
const angleInRadians = ((angleInDegrees - 90) * Math.PI) / 180.0;
|
||||||
return {
|
return {
|
||||||
x: center + radius * Math.cos(angleInRadians),
|
x: Math.round(center + radius * Math.cos(angleInRadians)),
|
||||||
y: center + radius * Math.sin(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 (
|
return (
|
||||||
<div className={styles.radar}>
|
<div className={styles.radar}>
|
||||||
<svg
|
<svg
|
||||||
@@ -80,7 +123,7 @@ export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
|
|||||||
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
|
||||||
>
|
>
|
||||||
{quadrants.map((quadrant) => (
|
{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)}
|
{renderGlow(quadrant.position, quadrant.color)}
|
||||||
{rings.map((ring) => (
|
{rings.map((ring) => (
|
||||||
<path
|
<path
|
||||||
@@ -94,6 +137,7 @@ export const Radar: FC<RadarProps> = ({ size = 800, quadrants, rings }) => {
|
|||||||
))}
|
))}
|
||||||
</g>
|
</g>
|
||||||
))}
|
))}
|
||||||
|
<g>{items.map((item) => renderItem(item))}</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export function getAppName() {
|
|||||||
return messages.radarName;
|
return messages.radarName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getChartConfig() {
|
||||||
|
return config.chart;
|
||||||
|
}
|
||||||
|
|
||||||
export function getFlag(flag: Exclude<Flag, Flag.Default>) {
|
export function getFlag(flag: Exclude<Flag, Flag.Default>) {
|
||||||
return config.flags[flag];
|
return config.flags[flag];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface Item {
|
|||||||
flag: Flag;
|
flag: Flag;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
revisions?: Revision[];
|
revisions?: Revision[];
|
||||||
|
random?: [radius: number, angle: number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Ring {
|
export interface Ring {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const Home: CustomPage = () => {
|
|||||||
{appName}{" "}
|
{appName}{" "}
|
||||||
<span style={{ color: "var(--highlight)" }}>Version #{version}</span>
|
<span style={{ color: "var(--highlight)" }}>Version #{version}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Radar quadrants={quadrants} rings={rings} />
|
<Radar quadrants={quadrants} rings={rings} items={items} />
|
||||||
<QuadrantList items={items} />
|
<QuadrantList items={items} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user