Move into folders
This commit is contained in:
parent
ee73ab77fd
commit
5cbb180c57
24 changed files with 181 additions and 185 deletions
83
frontend/src/components/map/StackedBarChart.tsx
Normal file
83
frontend/src/components/map/StackedBarChart.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { useMemo } from 'react';
|
||||
import { SEGMENT_COLORS } from '../../lib/consts';
|
||||
import { formatValue } from '../../lib/format';
|
||||
|
||||
interface Segment {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface StackedBarChartProps {
|
||||
segments: Segment[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
/** 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 }: StackedBarChartProps) {
|
||||
const sortedSegments = useMemo(
|
||||
() => [...segments].sort((a, b) => b.value - a.value),
|
||||
[segments]
|
||||
);
|
||||
|
||||
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: SEGMENT_COLORS[i % SEGMENT_COLORS.length],
|
||||
}}
|
||||
title={`${shortenLabel(segment.name)}: ${formatValue(segment.value)} (${pct.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: 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>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue