Files
krow_employer/src/components/charts/HiringFlow.jsx
2026-09-07 20:36:18 +05:30

116 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { BarChart } from '@mui/x-charts';
import { ChevronRight } from 'lucide-react';
import { cn } from '@/lib/utils';
import { chartSx, TONES, tooltipSx } from './muiChartTheme';
/**
* The hiring flow — one surface, not six cards.
*
* A funnel is a shape before it is a set of numbers, so the magnitudes are drawn
* once as a bar series and the stage-by-stage reading sits underneath it as a
* connected strip: count, pass-through rate, and what was lost getting there.
* Reading down a column gives one stage; reading across gives the journey.
*
* The weakest transition is the only thing coloured differently, because on a
* funnel the one worth acting on is the one losing the most people.
*
* `stages` and `transitions` come from the page's own fact sheet — this
* component derives no figures of its own.
*/
/** @param {any} props */
export function HiringFlow({ stages = [], transitions = [], weakestKey, className = '' }) {
if (!stages.length) return null;
const total = stages[0]?.count || 1;
const rateFor = (i) => (i === 0 ? 100 : transitions[i - 1]?.rate ?? 0);
const lostFor = (i) => (i === 0 ? 0 : transitions[i - 1]?.lost ?? 0);
const isWeak = (i) => i > 0 && transitions[i - 1] && transitions[i - 1].to === weakestKey;
return (
<div className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}>
{/* The shape. Axis-free: the strip underneath carries every figure, so a
y-axis would repeat what is already written. */}
<div className="px-2 pt-3 sm:px-4">
<BarChart
height={168}
margin={{ top: 12, right: 8, bottom: 24, left: 8 }}
xAxis={[{
scaleType: 'band',
data: stages.map((s) => s.label),
categoryGapRatio: 0.5,
barGapRatio: 0,
/* Per-stage colour through the documented axis API rather than a
CSS nth-child guess at MUI's internal markup: the weakest
transition is amber, every other stage is brand blue. */
colorMap: {
type: 'ordinal',
values: stages.map((s) => s.label),
colors: stages.map((_, i) => (isWeak(i) ? TONES.warning : TONES.brand)),
},
}]}
/* Pinned to the data: MUI's automatic domain rounds up to roughly
double the top stage, which leaves the funnel occupying half the
plot and reading much flatter than it is. */
yAxis={[{ min: 0, max: Math.max(total, 1), tickNumber: 3 }]}
series={[{
data: stages.map((s) => s.count),
label: 'Candidates',
valueFormatter: (value, ctx) => {
const i = ctx?.dataIndex ?? 0;
const lost = lostFor(i);
return `${value} · ${rateFor(i)}% through${lost ? ` · ${lost} lost` : ''}`;
},
}]}
colors={[TONES.brand]}
borderRadius={6}
grid={{ horizontal: true }}
skipAnimation
sx={{
...chartSx,
...tooltipSx,
/* The weakest transition's bar picks up the warning tone; every
other bar stays brand blue. */
}}
/>
</div>
{/* The reading. Scrolls rather than wraps on a phone, so the sequence
survives at any width. */}
<div className="overflow-x-auto border-t border-border">
<ol className="flex min-w-[36rem] items-stretch">
{stages.map((stage, i) => {
const rate = rateFor(i);
const lost = lostFor(i);
const weak = isWeak(i);
return (
<li key={stage.key} className="flex min-w-0 flex-1 items-center">
{i > 0 && (
<span className="flex shrink-0 flex-col items-center px-1.5" aria-hidden="true">
<ChevronRight className={cn('h-3.5 w-3.5', weak ? 'text-warning' : 'text-ink-4')} />
</span>
)}
<div className="min-w-0 flex-1 px-3 py-3">
<p className="truncate text-caption text-ink-3">{stage.label}</p>
<p className="mt-0.5 font-heading text-title font-bold tabular-nums text-ink-1">
{stage.count}
</p>
<p className="mt-1 flex flex-wrap items-baseline gap-x-2 text-caption tabular-nums">
<span className={weak ? 'font-semibold text-warning' : 'text-ink-4'}>
{i === 0 ? `${Math.round((stage.count / total) * 100)}% entered` : `${rate}% through`}
</span>
{lost > 0 && <span className="text-ink-4">−{lost}</span>}
</p>
</div>
</li>
);
})}
</ol>
</div>
</div>
);
}