/* global React */ // trajectory.jsx — three variants: spark, strip, ledger // Exposes window.Trajectory, window.SparkTrj, window.StripTrj, window.LedgerTrj const { useState } = React; function dirOf(trj) { if (!trj || trj.length < 2) return "mixed"; const first = trj[0], last = trj[trj.length - 1]; if (last === 0 && first > 0) return "down"; if (first === 0 && last > 0) return "up"; if (last > first) return "up"; if (last < first) return "down"; return "mixed"; } function deltaLabel(trj, kind) { if (!trj) return "—"; const first = trj[0], last = trj[trj.length - 1]; if (kind === "dodged") return "DODGED"; if (last === 0 && first > 0) return "FADED"; if (first === 0 && last > 0) return "EMERGED"; if (last > first) return `+${last - first}`; if (last < first) return `−${first - last}`; return "FLAT"; } // Variant A — sparkline + delta chip (inline density) function SparkTrj({ trj, kind = "emerged", quarters }) { if (!trj) { return ( — · — · — · — DODGED ); } const dir = kind === "dodged" ? "dodged" : kind === "disappeared" ? "down" : kind === "emerged" ? "up" : dirOf(trj); return ( {trj.map((n, i) => ( {n} {i < trj.length - 1 && } ))} {deltaLabel(trj, kind)} ); } // Variant B — quarter strip with hover quotes (for drill-down) function StripTrj({ trj, kind = "emerged", quarters, peakIndex, onCellClick }) { if (!trj) return null; const max = Math.max(...trj, 1); const peakIdx = peakIndex != null ? peakIndex : trj.indexOf(Math.max(...trj)); const [hoverIdx, setHoverIdx] = useState(null); return (
Mentions per quarter
{trj.map((n, i) => (
setHoverIdx(i)} onMouseLeave={() => setHoverIdx(null)} onClick={() => onCellClick && onCellClick(i)} >
{quarters[i]}
{n}
))}
); } // Variant C — stacked-bar mention ledger (where the topic lives) // segments: [opening, qa, financial] per quarter function LedgerTrj({ trj, kind = "emerged", quarters, segments }) { if (!trj) return null; const max = Math.max(...trj, 1); // Synthetic split if segments missing: weighted to opening for emerged, qa for disappeared const splits = segments || trj.map(n => { if (n === 0) return [0, 0, 0]; if (kind === "emerged") return [Math.round(n*0.6), Math.round(n*0.3), n - Math.round(n*0.6) - Math.round(n*0.3)]; if (kind === "disappeared") return [Math.round(n*0.25), Math.round(n*0.6), n - Math.round(n*0.25) - Math.round(n*0.6)]; return [Math.round(n*0.4), Math.round(n*0.4), n - 2*Math.round(n*0.4)]; }); return (
{trj.map((n, i) => { const s = splits[i] || [0,0,0]; const totalH = (n / max) * 80; return (
{n === 0 ? null : [0,1,2].map(si => (
))}
{quarters[i]}
{n}
); })}
); } Object.assign(window, { SparkTrj, StripTrj, LedgerTrj });