86 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|