diff --git a/scripts/buildData.ts b/scripts/buildData.ts index 2e8cffb..89c61d0 100644 --- a/scripts/buildData.ts +++ b/scripts/buildData.ts @@ -6,9 +6,16 @@ import { markedHighlight } from "marked-highlight"; import path from "path"; import config from "../next.config.mjs"; +import Positioner from "./positioner"; +import { getChartConfig, getQuadrants, getRings } from "@/lib/data"; import { Flag, Item } from "@/lib/types"; +const rings = getRings(); +const quadrants = getQuadrants(); +const { size } = getChartConfig(); +const positioner = new Positioner(size, quadrants, rings); + const marked = new Marked( markedHighlight({ langPrefix: "hljs language-", @@ -72,6 +79,7 @@ async function parseDirectory(dirPath: string): Promise { flag: Flag.Default, tags: data.tags || [], revisions: [], + position: [0, 0], }; } else { items[id].release = releaseDate; @@ -146,8 +154,7 @@ 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], + position: positioner.getNextPosition(item.quadrant, item.ring), flag: getFlag(item, latestRelease), // only keep revision which ring or body is different revisions: item.revisions diff --git a/scripts/positioner.ts b/scripts/positioner.ts new file mode 100644 index 0000000..63e0bf8 --- /dev/null +++ b/scripts/positioner.ts @@ -0,0 +1,95 @@ +import { Quadrant, Ring } from "@/lib/types"; + +type Position = [x: number, y: number]; +type RingDimension = [innerRadius: number, outerRadius: number]; + +// Corresponding to positions 1, 2, 3, and 4 respectively +const startAngles = [270, 0, 180, 90]; + +export default class Positioner { + private readonly centerRadius: number; + private readonly minDistance: number = 20; + private readonly paddingRing: number = 15; + private readonly paddingAngle: number = 10; + private positions: Record = {}; + private ringDimensions: Record = {}; + private quadrantAngles: Record = {}; + + constructor(size: number, quadrants: Quadrant[], rings: Ring[]) { + this.centerRadius = size / 2; + + quadrants.forEach((quadrant) => { + this.quadrantAngles[quadrant.id] = startAngles[quadrant.position - 1]; + }); + + rings.forEach((ring, index) => { + const innerRadius = + (rings[index - 1]?.radius ?? 0) * this.centerRadius + this.paddingRing; + const outerRadius = + (ring.radius ?? 1) * this.centerRadius - this.paddingRing; + this.ringDimensions[ring.id] = [innerRadius, outerRadius]; + }); + } + + static getDistance(a: Position, b: Position): number { + const [x1, y1] = a; + const [x2, y2] = b; + return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2); + } + + private isOverlapping(position: Position, positions: Position[]): boolean { + return positions.some( + (p) => Positioner.getDistance(position, p) < this.minDistance, + ); + } + + private getXYPosition( + quadrantId: string, + ringId: string, + radiusFraction: number, + angleFraction: number, + ): Position { + const [innerRadius, outerRadius] = this.ringDimensions[ringId]; + const ringWidth = outerRadius - innerRadius; + const absoluteRadius = innerRadius + radiusFraction * ringWidth; + + const startAngle = this.quadrantAngles[quadrantId] + this.paddingAngle; + const endAngle = startAngle + 90 - 2 * this.paddingAngle; + const absoluteAngle = startAngle + (endAngle - startAngle) * angleFraction; + const angleInRadians = ((absoluteAngle - 90) * Math.PI) / 180; + + return [ + Math.round(this.centerRadius + absoluteRadius * Math.cos(angleInRadians)), + Math.round(this.centerRadius + absoluteRadius * Math.sin(angleInRadians)), + ]; + } + + public getNextPosition(quadrantId: string, ringId: string): Position { + this.positions[quadrantId] ??= []; + + let tries = 0; + let position: Position; + + do { + position = this.getXYPosition( + quadrantId, + ringId, + Math.sqrt(Math.random()), + Math.random(), + ); + tries++; + } while ( + this.isOverlapping(position, this.positions[quadrantId]) && + tries < 150 + ); + + if (tries >= 150) { + console.warn( + `Could not find a non-overlapping position for ${quadrantId} in ring ${ringId}`, + ); + } + + this.positions[quadrantId].push(position); + return position; + } +} diff --git a/src/components/Radar/Chart.tsx b/src/components/Radar/Chart.tsx index c7311d6..ebdfeb9 100644 --- a/src/components/Radar/Chart.tsx +++ b/src/components/Radar/Chart.tsx @@ -86,29 +86,8 @@ const _Chart: FC = ({ 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 [x, y] = item.position; - 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 ( { const router = useRouter(); const tag = router.query.tag as string | undefined; const appName = getAppName(); + const chartConfig = getChartConfig(); const version = getReleases().length; const rings = getRings(); const quadrants = getQuadrants(); @@ -33,7 +35,12 @@ const Home: CustomPage = () => { Version #{version} - +