o2o-castad-frontend/ado2-marketing-intelligence.../components/GeometricChart.tsx

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>
);
};