159 lines
5.2 KiB
TypeScript
Executable File
159 lines
5.2 KiB
TypeScript
Executable File
import React from 'react';
|
|
import { USP } from '../types';
|
|
|
|
interface GeometricChartProps {
|
|
data: USP[];
|
|
}
|
|
|
|
export const GeometricChart: React.FC<GeometricChartProps> = ({ data }) => {
|
|
// Increased canvas size to prevent labels (especially on the right side like 'Privacy') from being cut off
|
|
const size = 500;
|
|
const center = size / 2;
|
|
const radius = 110; // Slightly increased radius for better visibility
|
|
const sides = data.length;
|
|
|
|
// Mint Color #94FBE0
|
|
const accentColor = "#94FBE0";
|
|
|
|
// Calculate polygon points
|
|
const getPoints = (r: number) => {
|
|
return data.map((_, i) => {
|
|
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
|
|
const x = center + r * Math.cos(angle);
|
|
const y = center + r * Math.sin(angle);
|
|
return `${x},${y}`;
|
|
}).join(' ');
|
|
};
|
|
|
|
// Calculate data points based on score
|
|
const getDataPoints = () => {
|
|
return data.map((item, i) => {
|
|
const normalizedScore = item.score / 100;
|
|
const r = radius * normalizedScore;
|
|
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
|
|
const x = center + r * Math.cos(angle);
|
|
const y = center + r * Math.sin(angle);
|
|
return `${x},${y}`;
|
|
}).join(' ');
|
|
};
|
|
|
|
// Calculate label positions (pushed out slightly further)
|
|
const labelRadius = radius + 55; // Increased padding for labels
|
|
const labels = data.map((item, i) => {
|
|
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
|
|
const x = center + labelRadius * Math.cos(angle);
|
|
const y = center + labelRadius * Math.sin(angle);
|
|
|
|
// Adjust text anchor based on position
|
|
let anchor: 'start' | 'middle' | 'end' = 'middle';
|
|
if (x < center - 20) anchor = 'end';
|
|
if (x > center + 20) anchor = 'start';
|
|
|
|
return { x, y, text: item.label, sub: item.subLabel, anchor, score: item.score };
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col items-center justify-center py-2 relative w-full h-full">
|
|
<svg viewBox={`0 0 ${size} ${size}`} className="w-full h-auto max-w-[500px]" style={{ overflow: 'visible' }}>
|
|
<defs>
|
|
<radialGradient id="polyGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
|
<stop offset="0%" stopColor={accentColor} stopOpacity="0.3" />
|
|
<stop offset="100%" stopColor={accentColor} stopOpacity="0.05" />
|
|
</radialGradient>
|
|
</defs>
|
|
|
|
{/* Background Grids (Concentric) - Increased Opacity for Visibility */}
|
|
{[1, 0.75, 0.5, 0.25].map((scale, i) => (
|
|
<polygon
|
|
key={i}
|
|
points={getPoints(radius * scale)}
|
|
fill="none"
|
|
stroke={accentColor}
|
|
strokeOpacity={0.25 - (0.02 * i)}
|
|
strokeWidth="1"
|
|
strokeDasharray={i === 0 ? "0" : "4 2"}
|
|
/>
|
|
))}
|
|
|
|
{/* Axes Lines - Increased Opacity */}
|
|
{data.map((_, i) => {
|
|
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
|
|
const x = center + radius * Math.cos(angle);
|
|
const y = center + radius * Math.sin(angle);
|
|
return (
|
|
<line
|
|
key={i}
|
|
x1={center}
|
|
y1={center}
|
|
x2={x}
|
|
y2={y}
|
|
stroke={accentColor}
|
|
strokeOpacity="0.25"
|
|
strokeWidth="1"
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* Data Shape */}
|
|
<polygon
|
|
points={getDataPoints()}
|
|
fill="url(#polyGradient)"
|
|
stroke={accentColor}
|
|
strokeWidth="2.5"
|
|
strokeLinejoin="round"
|
|
/>
|
|
|
|
{/* Data Points (Dots) - Increased base size and highlight size */}
|
|
{data.map((item, i) => {
|
|
const normalizedScore = item.score / 100;
|
|
const r = radius * normalizedScore;
|
|
const angle = (Math.PI * 2 * i) / sides - Math.PI / 2;
|
|
const x = center + r * Math.cos(angle);
|
|
const y = center + r * Math.sin(angle);
|
|
const isHigh = item.score >= 90;
|
|
return (
|
|
<g key={i}>
|
|
{isHigh && (
|
|
<circle cx={x} cy={y} r="10" fill={accentColor} fillOpacity="0.4" />
|
|
)}
|
|
{/* Base dot is brighter (white) for all, slightly smaller for non-high */}
|
|
<circle cx={x} cy={y} r={isHigh ? 5 : 4} fill="#fff" />
|
|
</g>
|
|
)
|
|
})}
|
|
|
|
{/* Labels */}
|
|
{labels.map((l, i) => {
|
|
const isHigh = l.score >= 90;
|
|
return (
|
|
<g key={i}>
|
|
<text
|
|
x={l.x}
|
|
y={l.y - 7}
|
|
textAnchor={l.anchor}
|
|
fill={isHigh ? "#fff" : "#e2e8f0"}
|
|
fontSize={isHigh ? "14" : "12"}
|
|
fontWeight={isHigh ? "700" : "600"}
|
|
className="tracking-tight"
|
|
style={{ fontFamily: "sans-serif" }}
|
|
>
|
|
{l.text}
|
|
</text>
|
|
<text
|
|
x={l.x}
|
|
y={l.y + 9}
|
|
textAnchor={l.anchor}
|
|
fill={isHigh ? accentColor : "#94a3b8"}
|
|
fontSize={isHigh ? "11" : "10"}
|
|
fontWeight={isHigh ? "600" : "400"}
|
|
className="uppercase tracking-wider"
|
|
>
|
|
{l.sub}
|
|
</text>
|
|
</g>
|
|
);
|
|
})}
|
|
</svg>
|
|
</div>
|
|
);
|
|
}; |