perfect-postcode/frontend/src/components/map/StackedBarChart.tsx

86 lines
2.8 KiB
TypeScript

import { useMemo } from 'react';
import { SEGMENT_COLORS } from '../../lib/consts';
import { formatValue, roundedPercentages } from '../../lib/format';
interface Segment {
name: string;
value: number;
}
interface StackedBarChartProps {
segments: Segment[];
total: number;
/** Optional custom colors keyed by segment name. Falls back to SEGMENT_COLORS. */
colorMap?: Record<string, string>;
}
/** Strip common suffixes/prefixes to produce short legend labels */
function shortenLabel(name: string): string {
return name
.replace(' (avg/yr)', '')
.replace(/^% /, '')
.replace('and sexual offences', '')
.replace('and arson', '')
.replace('from the person', '')
.replace('Possession of weapons', 'Weapons')
.replace('Anti-social behaviour', 'Anti-social')
.replace('Criminal damage', 'Damage')
.trim();
}
export default function StackedBarChart({ segments, total, colorMap }: StackedBarChartProps) {
const sortedSegments = useMemo(() => [...segments].sort((a, b) => b.value - a.value), [segments]);
const roundedPcts = useMemo(
() => roundedPercentages(sortedSegments.map((s) => s.value), total, 1),
[sortedSegments, total]
);
if (total === 0) {
return <div className="text-xs text-warm-400 dark:text-warm-500 italic">No data</div>;
}
return (
<div className="space-y-1.5">
{/* Stacked bar */}
<div className="flex h-4 rounded overflow-hidden bg-warm-200 dark:bg-warm-700">
{sortedSegments.map((segment, i) => {
const pct = (segment.value / total) * 100;
if (pct < 0.5) return null;
return (
<div
key={segment.name}
className="h-full"
style={{
width: `${pct}%`,
backgroundColor:
colorMap?.[segment.name] ?? SEGMENT_COLORS[i % SEGMENT_COLORS.length],
}}
title={`${shortenLabel(segment.name)}: ${formatValue(segment.value)} (${roundedPcts[i].toFixed(1)}%)`}
/>
);
})}
</div>
{/* Legend */}
<div className="flex flex-wrap gap-x-3 gap-y-0.5">
{sortedSegments.map((segment, i) => (
<div key={segment.name} className="flex items-center gap-1">
<span
className="w-2 h-2 rounded-sm shrink-0"
style={{
backgroundColor:
colorMap?.[segment.name] ?? SEGMENT_COLORS[i % SEGMENT_COLORS.length],
}}
/>
<span className="text-[10px] text-warm-600 dark:text-warm-400">
{shortenLabel(segment.name)}
</span>
<span className="text-[10px] text-warm-400 dark:text-warm-500">
{formatValue(segment.value)}
</span>
</div>
))}
</div>
</div>
);
}