Spaces:
Sleeping
Sleeping
Download apps/web/components/forest-plot.tsx from ayushgupta7777/resumematch-api: direct link, hf CLI and curl.
- Browser
- Download file 9.33 kB
-
https://huggingface.co/spaces/ayushgupta7777/resumematch-api/resolve/main/apps/web/components/forest-plot.tsx
- Command line
-
hf download hf://spaces/ayushgupta7777/resumematch-api/apps/web/components/forest-plot.tsx
-
curl -L -o forest-plot.tsx https://huggingface.co/spaces/ayushgupta7777/resumematch-api/resolve/main/apps/web/components/forest-plot.tsx
9.33 kB
| "use client"; | |
| import { Btn } from "@/components/btn"; | |
| import { | |
| Bar, | |
| BarChart, | |
| Cell, | |
| ErrorBar, | |
| LabelList, | |
| ReferenceLine, | |
| ResponsiveContainer, | |
| Tooltip, | |
| XAxis, | |
| YAxis, | |
| } from "recharts"; | |
| import type { ClusterRow } from "@/lib/types"; | |
| import { useTheme } from "@/lib/providers"; | |
| import { useState } from "react"; | |
| import { A, B, FF } from "@/lib/theme"; | |
| function Card({ children, title, subtitle }: { children: React.ReactNode; title: string; subtitle: string }) { | |
| const { t } = useTheme(); | |
| return ( | |
| <div style={{ background: "transparent", border: `1px solid var(--accent)`, borderRadius: 16, padding: 20 }}> | |
| <h3 style={{ fontFamily: FF.display, fontSize: 17, fontWeight: 700, letterSpacing: "-0.01em", margin: 0 }}> | |
| {title} | |
| </h3> | |
| <p style={{ fontSize: 13, color: t.muted, margin: "6px 0 0" }}>{subtitle}</p> | |
| {children} | |
| </div> | |
| ); | |
| } | |
| export function ForestPlot({ clusters }: { clusters: ClusterRow[] }) { | |
| const { t } = useTheme(); | |
| const [selected, setSelected] = useState<ClusterRow | null>(null); | |
| const [hovered, setHovered] = useState<number | null>(null); | |
| const rows = [...clusters] | |
| .filter((c) => c.mean_delta !== null) | |
| .sort((a, b) => (a.mean_delta ?? 0) - (b.mean_delta ?? 0)) | |
| .map((c) => { | |
| const x = (c.mean_delta ?? 0) * 100; | |
| const lo = (c.ci_low ?? 0) * 100; | |
| const hi = (c.ci_high ?? 0) * 100; | |
| const color = c.winner === "A" ? A : c.winner === "B" ? B : t.faint; | |
| return { label: c.label, x, err: [x - lo, hi - x] as [number, number], color, row: c }; | |
| }); | |
| return ( | |
| <Card | |
| title="Where each résumé matches better — by job cluster" | |
| subtitle="Average score gap per cluster (percentage points), with 95% bootstrap intervals." | |
| > | |
| {selected ? ( | |
| <DiffPanel row={selected} onBack={() => setSelected(null)} t={t} /> | |
| ) : ( | |
| <> | |
| <div style={{ height: 360, width: "100%", marginTop: 14 }}> | |
| <ResponsiveContainer> | |
| <BarChart layout="vertical" data={rows} margin={{ top: 8, right: 28, bottom: 8, left: 8 }}> | |
| <XAxis | |
| type="number" | |
| tick={{ fontSize: 11, fill: t.muted, fontFamily: FF.mono }} | |
| tickFormatter={(v: any) => v.toFixed(0)} | |
| stroke={t.border} | |
| /> | |
| <YAxis | |
| type="category" | |
| dataKey="label" | |
| width={156} | |
| tick={{ fontSize: 12, fill: t.text }} | |
| stroke={t.border} | |
| /> | |
| <ReferenceLine x={0} stroke={t.faint} strokeDasharray="3 3" /> | |
| <Tooltip | |
| cursor={{ fill: t.surface2, opacity: 0.5 }} | |
| contentStyle={{ | |
| background: t.surface, | |
| border: `1px solid ${t.border}`, | |
| borderRadius: 8, | |
| fontSize: 12, | |
| color: t.text, | |
| fontFamily: FF.mono, | |
| }} | |
| labelStyle={{ color: t.text }} | |
| itemStyle={{ color: t.text }} | |
| formatter={(v: any) => [`${v.toFixed(2)} pts`, "Δ (B − A)"]} | |
| /> | |
| <Bar | |
| dataKey="x" | |
| radius={2} | |
| isAnimationActive={false} | |
| cursor="pointer" | |
| onClick={(d: any) => setSelected(d?.payload?.row ?? null)} | |
| onMouseEnter={(_: any, i: number) => setHovered(i)} | |
| onMouseLeave={() => setHovered(null)} | |
| > | |
| <LabelList | |
| dataKey="x" | |
| content={(props: any) => { | |
| const { x, y, width, height, index, viewBox } = props; | |
| if (index !== hovered) return null; | |
| const label = "(click to see details)"; | |
| const px = 6.2 * label.length; // approx text width at 10.5px mono | |
| const barEnd = x + width; // right end for +bars, left end for -bars | |
| const rightEdge = (viewBox?.x ?? 0) + (viewBox?.width ?? 9999); | |
| const leftEdge = viewBox?.x ?? 0; | |
| let tx: number; | |
| let anchor: "start" | "end"; | |
| let inside = false; | |
| if (width >= 0) { | |
| if (barEnd + 8 + px <= rightEdge) { tx = barEnd + 8; anchor = "start"; } | |
| else { tx = barEnd - 8; anchor = "end"; inside = true; } | |
| } else { | |
| if (barEnd - 8 - px >= leftEdge) { tx = barEnd - 8; anchor = "end"; } | |
| else { tx = barEnd + 8; anchor = "start"; inside = true; } | |
| } | |
| const wide = Math.abs(width) > px + 20; | |
| return ( | |
| <text x={tx} y={y + height / 2} dy={3} textAnchor={anchor} | |
| style={{ fontFamily: FF.mono, fontSize: 10.5, fill: inside ? t.text : t.muted, letterSpacing: "0.02em" }}> | |
| {label} | |
| </text> | |
| ); | |
| }} | |
| /> | |
| {rows.map((r, i) => ( | |
| <Cell key={i} fill={r.color} fillOpacity={hovered === null || hovered === i ? 1 : 0.45} /> | |
| ))} | |
| <ErrorBar dataKey="err" direction="x" width={4} strokeWidth={1.2} stroke={t.faint} /> | |
| </Bar> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| <div | |
| className="flex items-center justify-between" | |
| style={{ fontFamily: FF.mono, fontSize: 10, color: t.faint, marginTop: 6, letterSpacing: "0.04em" }} | |
| > | |
| <span>{"\u25C0"} Résumé A matches better</span> | |
| <span>Résumé B matches better {"\u25B6"}</span> | |
| </div> | |
| </> | |
| )} | |
| </Card> | |
| ); | |
| } | |
| export function Chips({ | |
| items, | |
| color, | |
| t, | |
| empty = "no distinguishing skills in this segment", | |
| }: { | |
| items: { skill: string; freq: number }[]; | |
| color: string; | |
| t: any; | |
| empty?: string; | |
| }) { | |
| if (!items.length) | |
| return ( | |
| <span style={{ fontFamily: FF.mono, fontSize: 11.5, color: t.faint, letterSpacing: "0.04em" }}> | |
| {empty} | |
| </span> | |
| ); | |
| const max = Math.max(...items.map((i) => i.freq), 0.01); | |
| return ( | |
| <div style={{ display: "flex", flexDirection: "column", gap: 6 }}> | |
| {items.map((it, idx) => ( | |
| <div | |
| key={it.skill} | |
| style={{ | |
| position: "relative", | |
| display: "flex", | |
| alignItems: "center", | |
| justifyContent: "space-between", | |
| overflow: "hidden", | |
| borderRadius: 8, | |
| border: `1px solid ${color}${idx === 0 ? "" : "44"}`, | |
| padding: "5px 11px", | |
| background: "transparent", | |
| }} | |
| > | |
| {/* frequency bar — structure encodes the % */} | |
| <span | |
| style={{ | |
| position: "absolute", inset: 0, width: `${(it.freq / max) * 100}%`, | |
| background: color, opacity: idx === 0 ? 0.16 : 0.09, | |
| }} | |
| /> | |
| <span | |
| style={{ | |
| position: "relative", fontFamily: FF.mono, | |
| fontSize: idx === 0 ? 13 : 12, | |
| fontWeight: idx === 0 ? 700 : 400, | |
| color: t.text, letterSpacing: "0.01em", | |
| }} | |
| > | |
| {it.skill} | |
| </span> | |
| <span style={{ position: "relative", fontFamily: FF.mono, fontSize: 11, color: t.muted }}> | |
| {Math.round(it.freq * 100)}% | |
| </span> | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function DiffPanel({ row, onBack, t }: { row: ClusterRow; onBack: () => void; t: any }) { | |
| const d = row.differentiators ?? { a_favoring: [], b_favoring: [] }; | |
| return ( | |
| <div style={{ marginTop: 14, animation: "fadeIn 220ms ease" }}> | |
| <style>{"@keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}"}</style> | |
| <Btn | |
| variant="ghost" | |
| onClick={onBack} | |
| style={{ | |
| gap: 8, marginBottom: 20, | |
| fontFamily: FF.mono, fontSize: 11, fontWeight: 600, textTransform: "uppercase", | |
| letterSpacing: "0.08em", padding: "7px 15px", | |
| }} | |
| > | |
| <span style={{ fontSize: 14, lineHeight: 1 }}>{"\u2190"}</span> back to all clusters | |
| </Btn> | |
| <p style={{ fontFamily: FF.display, fontSize: 15, fontWeight: 700, color: t.text, margin: "0 0 4px" }}> | |
| {row.label} | |
| </p> | |
| <p style={{ fontSize: 12.5, color: t.muted, margin: "0 0 16px" }}> | |
| Skills common in this cluster's postings that appear in only one résumé — what separates | |
| A from B here. (The score itself is semantic similarity, so read these as differentiators, not exact causes.) | |
| </p> | |
| <div | |
| style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 24 }} | |
| className="max-[560px]:!grid-cols-1" | |
| > | |
| <div> | |
| <p style={{ fontFamily: FF.mono, fontSize: 10.5, color: A, margin: "0 0 10px", letterSpacing: "0.08em" }}> | |
| {"\u25C0"} FAVORS RÉSUMÉ A | |
| </p> | |
| <Chips items={d.a_favoring} color={A} t={t} /> | |
| </div> | |
| <div> | |
| <p style={{ fontFamily: FF.mono, fontSize: 10.5, color: B, margin: "0 0 10px", letterSpacing: "0.08em" }}> | |
| FAVORS RÉSUMÉ B {"\u25B6"} | |
| </p> | |
| <Chips items={d.b_favoring} color={B} t={t} /> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |