Files
Project_Velocity/app/src/oracle/components/renderers/GeoMapRenderer.tsx
2026-04-12 02:01:36 +05:30

116 lines
4.9 KiB
TypeScript

import { motion } from 'framer-motion';
import type { CanvasComponent } from '../../types/canvas';
import { RendererWrapper, NoDataPlaceholder, type ComponentRenderContext } from './RendererWrapper';
interface Props { component: CanvasComponent; ctx: ComponentRenderContext }
interface GeoRow {
district: string;
lat?: number;
lng?: number;
lead_count?: number;
count?: number;
avg_qd_score?: number;
x?: number;
y?: number;
}
export function GeoMapRenderer({ component, ctx }: Props) {
const rows = (component.dataRows ?? []) as unknown as GeoRow[];
const intensityField = (component.visualizationParameters as { intensityField?: string }).intensityField ?? 'lead_count';
const tooltipFields = (component.visualizationParameters as { tooltipFields?: string[] }).tooltipFields ?? ['district', 'lead_count'];
if (!rows.length) return (
<RendererWrapper component={component} ctx={ctx} minHeight={360}>
<NoDataPlaceholder message="No geographic data available." />
</RendererWrapper>
);
const maxVal = Math.max(...rows.map((r) => Number(r[intensityField as keyof GeoRow] ?? r.count ?? r.lead_count ?? 0)));
return (
<RendererWrapper component={component} ctx={ctx} minHeight={400}>
<div className="relative w-full h-[280px] rounded-xl overflow-hidden" style={{
background: 'radial-gradient(ellipse at center, rgba(14,116,144,0.12) 0%, rgba(10,12,20,0.95) 100%)',
border: '1px solid rgba(59,130,246,0.12)',
}}>
{/* Dubai grid lines (decorative) */}
<svg className="absolute inset-0 w-full h-full opacity-10" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#3B82F6" strokeWidth="0.5" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
{/* Coastline glow */}
<div
className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none"
style={{ background: 'linear-gradient(to top, rgba(14,116,144,0.15) 0%, transparent 100%)' }}
/>
{/* Legend */}
<div
className="absolute left-3 top-3 z-10 px-3 py-2.5 rounded-xl text-xs"
style={{ background: 'rgba(10,12,20,0.88)', border: '1px solid rgba(255,255,255,0.08)', backdropFilter: 'blur(8px)' }}
>
<p className="text-zinc-500 uppercase tracking-wider text-[10px] mb-2">Lead Intensity</p>
{[
{ label: 'High', color: '#0EA5E9' },
{ label: 'Medium', color: '#22D3EE' },
{ label: 'Low', color: '#164E63' },
].map(({ label, color }) => (
<div key={label} className="flex items-center gap-2 mb-1">
<div className="w-2 h-2 rounded-full" style={{ background: color, boxShadow: `0 0 8px ${color}` }} />
<span className="text-zinc-300">{label}</span>
</div>
))}
</div>
{/* District pins */}
{rows.map((row, i) => {
const val = Number(row[intensityField as keyof GeoRow] ?? row.count ?? row.lead_count ?? 0);
const ratio = maxVal > 0 ? val / maxVal : 0;
const color = ratio > 0.7 ? '#0EA5E9' : ratio > 0.4 ? '#22D3EE' : '#164E63';
const size = 6 + ratio * 14;
const x = row.x ?? (20 + i * 12);
const y = row.y ?? (30 + i * 8);
return (
<motion.div
key={row.district}
className="absolute group cursor-pointer"
style={{ left: `${x}%`, top: `${y}%`, transform: 'translate(-50%, -50%)' }}
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: i * 0.08, type: 'spring', damping: 10 }}
>
<motion.div
className="rounded-full"
style={{
width: size,
height: size,
background: color,
boxShadow: `0 0 ${size * 2}px ${color}88`,
}}
whileHover={{ scale: 1.5 }}
/>
{/* Tooltip */}
<div
className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2.5 py-1.5 rounded-lg text-xs whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-30"
style={{ background: 'rgba(10,12,20,0.95)', border: '1px solid rgba(59,130,246,0.2)', backdropFilter: 'blur(8px)' }}
>
<p className="font-semibold text-zinc-100">{row.district}</p>
{tooltipFields.map((f) => (
<p key={f} className="text-zinc-400 capitalize">{f.replace(/_/g, ' ')}: {String(row[f as keyof GeoRow] ?? '—')}</p>
))}
</div>
</motion.div>
);
})}
</div>
</RendererWrapper>
);
}