ClaimsWordmap / src /App.jsx
Keith Yu
clean repo w/o node_modules
dbc23dd
import { useState } from "react";
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from "recharts";
const keywords = [
{ word: "renewable", runs: [88, 85, 78], cat: "persist" },
{ word: "emissions", runs: [62, 95, 72], cat: "persist" },
{ word: "energy", runs: [78, 70, 82], cat: "persist" },
{ word: "technology", runs: [30, 50, 95], cat: "grow" },
{ word: "greenhouse", runs: [40, 35, 55], cat: "persist" },
{ word: "biofuels", runs: [95, 15, 5], cat: "shrink" },
{ word: "algae", runs: [55, 5, 0], cat: "shrink" },
{ word: "ethanol", runs: [50, 5, 0], cat: "shrink" },
{ word: "cellulosic", runs: [38, 0, 0], cat: "shrink" },
{ word: "biodiesel", runs: [42, 0, 0], cat: "shrink" },
{ word: "carbon capture", runs: [65, 45, 20], cat: "shrink" },
{ word: "digital", runs: [0, 55, 25], cat: "emerge15" },
{ word: "AI", runs: [0, 58, 20], cat: "emerge15" },
{ word: "economic", runs: [10, 52, 35], cat: "emerge15" },
{ word: "natural gas", runs: [25, 55, 30], cat: "emerge15" },
{ word: "plastics", runs: [0, 38, 15], cat: "emerge15" },
{ word: "sustainability", runs: [20, 65, 50], cat: "emerge15" },
{ word: "infrastructure", runs: [15, 42, 30], cat: "emerge15" },
{ word: "offshore", runs: [0, 0, 62], cat: "emerge50" },
{ word: "monitoring", runs: [0, 0, 70], cat: "emerge50" },
{ word: "marine fuels", runs: [0, 0, 48], cat: "emerge50" },
{ word: "exploration", runs: [0, 0, 55], cat: "emerge50" },
{ word: "CO2", runs: [15, 20, 58], cat: "emerge50" },
{ word: "storage", runs: [5, 10, 52], cat: "emerge50" },
{ word: "exports", runs: [0, 0, 45], cat: "emerge50" },
{ word: "power gen", runs: [0, 5, 50], cat: "emerge50" },
{ word: "facility", runs: [0, 0, 42], cat: "emerge50" },
{ word: "electronics", runs: [0, 0, 48], cat: "emerge50" },
];
const runLabels = ["5-Article", "15-Article", "50-Article"];
const runColors = ["#0D9488", "#3B82F6", "#7C3AED"];
const catColors = { persist: "#16A34A", grow: "#0D9488", shrink: "#DC2626", emerge15: "#3B82F6", emerge50: "#7C3AED" };
const catLabels = { persist: "Persistent across all runs", grow: "Grows with scale", shrink: "Fades at scale", emerge15: "Emerges at 15 articles", emerge50: "Emerges at 50 articles" };
// ── Full SC descriptions ──────────────────────────────────────────────────────
const scFullText = {
"SC_1": "Natural gas as a climate transition fuel — claims positioning natural gas as a bridge or essential part of the transition to clean energy",
"SC_2": "Oil operations and sustainability — claims that oil extraction and production are managed sustainably or responsibly",
"SC_3": "Carbon capture and storage (CCUS) viability — claims that CCS technology is safe, available, scalable, and essential for decarbonisation",
"SC_4": "Investment in renewable energy — claims that fossil fuel companies are actively investing in and enabling renewable energy",
"SC_5": "Addressing climate change — claims that the company is taking meaningful action to address climate change and environmental impacts",
"SC_6": "Digital technology and AI for climate — claims that digital innovation and AI are being deployed to solve climate and energy challenges",
"SC_7": "Economic development and growth — claims that fossil fuel activity drives jobs, economic growth, and energy security",
};
const scParagraphData = [
{ sc: "SC_1 Nat gas", r5: 0, r15: 44, r50: 100, scKey: "SC_1" },
{ sc: "SC_2 Oil", r5: 2, r15: 17, r50: 66, scKey: "SC_2" },
{ sc: "SC_3 CCUS", r5: 26, r15: 14, r50: 53, scKey: "SC_3" },
{ sc: "SC_4 Renewable", r5: 36, r15: 8, r50: 130, scKey: "SC_4" },
{ sc: "SC_5 Climate", r5: 18, r15: 93, r50: 321, scKey: "SC_5" },
{ sc: "SC_6 Digital/AI", r5: 2, r15: 24, r50: 20, scKey: "SC_6" },
{ sc: "SC_7 Economic", r5: 9, r15: 13, r50: 49, scKey: "SC_7" },
];
// ── Top NCs per run with full text from claim_history JSONs ───────────────────
const topNCs = [
// 5-article
[
{ nc: "NC_22", label: "Algae/bacteria grow in diverse environments", fullText: "Single-cell organisms like algae and bacteria can grow in diverse environments without competing with food production", count: 4, sc: "SC_4" },
{ nc: "NC_18", label: "Algae biofuels power diesel engines", fullText: "Algae biofuels can power existing diesel engines, enabling cleaner fossil fuel alternatives", count: 3, sc: "SC_4" },
{ nc: "NC_19", label: "Next-gen biofuels as sustainable energy", fullText: "Next generation biofuels as sustainable and environmentally friendly energy sources", count: 3, sc: "SC_4" },
{ nc: "NC_23", label: "Saltwater algae can produce oil directly", fullText: "Saltwater algae can produce oil directly", count: 3, sc: "SC_4" },
{ nc: "NC_24", label: "Bacteria unlock energy from plant waste", fullText: "Bacteria can unlock energy from plant waste materials like cornhusks and sawdust without competing with food production", count: 3, sc: "SC_5" },
{ nc: "NC_46", label: "Building world-scale blue hydrogen facility", fullText: "We are building a world-scale blue hydrogen facility", count: 3, sc: "SC_3" },
{ nc: "NC_6", label: "Carbon capture is safe", fullText: "Carbon capture is safe", count: 2, sc: "SC_3" },
{ nc: "NC_7", label: "Carbon capture needed to fight climate", fullText: "Carbon capture is needed to fight climate change", count: 2, sc: "SC_3" },
{ nc: "NC_10", label: "Carbon capture is widely available", fullText: "Carbon capture is widely available", count: 2, sc: "SC_3" },
{ nc: "NC_17", label: "Algae biofuels: lower-carbon diesel alt.", fullText: "Biofuels from algae are lower-carbon alternatives to diesel", count: 2, sc: "SC_4" },
],
// 15-article
[
{ nc: "NC_6", label: "Carbon capture is safe", fullText: "Carbon capture is safe", count: 6, sc: "SC_3" },
{ nc: "NC_153", label: "Reusing produced water reduces env. impact", fullText: "Reusing produced water reduces environmental impact by minimizing freshwater use and wastewater discharge", count: 4, sc: "SC_5" },
{ nc: "NC_23", label: "CCS permanently stores CO2 underground", fullText: "CCS permanently stores CO2 underground to prevent atmospheric emissions", count: 3, sc: "SC_3" },
{ nc: "NC_36", label: "LNG supply will rapidly expand", fullText: "Claim that LNG supply will rapidly expand in coming years", count: 3, sc: "SC_1" },
{ nc: "NC_38", label: "Policies encourage switching coal to gas", fullText: "Environmental policies encourage switching from coal to gas-fired power generation", count: 3, sc: "SC_1" },
{ nc: "NC_154", label: "Water use in oil ops contributes to sustain.", fullText: "Claim that water use in oil field operations contributes to sustainability management", count: 3, sc: "SC_2" },
{ nc: "NC_159", label: "Oil development must avoid stressing water", fullText: "Claim that oil development and production must avoid stressing water supply", count: 3, sc: "SC_2" },
{ nc: "NC_7", label: "Carbon capture needed to fight climate", fullText: "Carbon capture is needed to fight climate change", count: 2, sc: "SC_3" },
{ nc: "NC_30", label: "Mobility-as-a-service challenges vehicle use", fullText: "Claim that mobility as a service challenges personal vehicle ownership primarily in urban centers", count: 2, sc: "SC_5" },
{ nc: "NC_35", label: "LNG industry growth in global gas market", fullText: "LNG industry growth as a key part of global natural gas consumption", count: 2, sc: "SC_1" },
],
// 50-article
[
{ nc: "NC_87", label: "CCS is key tech to unlock low-carbon future", fullText: "The claim that carbon capture and storage (CCS) is a key technology to unlock a lower-emission future", count: 12, sc: "SC_3" },
{ nc: "NC_1", label: "Natural gas reduces emissions", fullText: "Natural gas reduces emissions", count: 11, sc: "SC_1" },
{ nc: "NC_56", label: "LNG bunker as essential transition solution", fullText: "Immediate availability of LNG bunker as an essential transition solution", count: 10, sc: "SC_1" },
{ nc: "NC_57", label: "LNG propulsion reduces environmental harm", fullText: "LNG propulsion reduces emissions or environmental harm", count: 9, sc: "SC_5" },
{ nc: "NC_7", label: "Carbon capture needed to fight climate", fullText: "Carbon capture is needed to fight climate change", count: 8, sc: "SC_3" },
{ nc: "NC_2", label: "Natural gas integral to climate transition", fullText: "Natural gas is integral to the climate transition", count: 7, sc: "SC_1" },
{ nc: "NC_6", label: "Carbon capture is safe", fullText: "Carbon capture is safe", count: 7, sc: "SC_3" },
{ nc: "NC_120", label: "Commitment to limit env. impacts on water", fullText: "Claim of commitment to limit environmental impacts on water resources including reinjection of produced water into reservoirs", count: 7, sc: "SC_5" },
{ nc: "NC_277", label: "Nat. gas essential across multiple sectors", fullText: "Natural gas is essential across multiple sectors including jobs, electricity, heating, and manufacturing", count: 7, sc: "SC_1" },
{ nc: "NC_3", label: "Oil spills are part of the everyday", fullText: "Oil spills are part of the every day", count: 6, sc: "SC_2" },
],
];
const scColorMap = {
"SC_1": "#0D9488", "SC_2": "#6B7280", "SC_3": "#D97706",
"SC_4": "#16A34A", "SC_5": "#3B82F6", "SC_6": "#7C3AED", "SC_7": "#EC4899",
};
const scLabelMap = {
"SC_1": "Nat gas", "SC_2": "Oil", "SC_3": "CCUS",
"SC_4": "Renewable", "SC_5": "Climate", "SC_6": "Digital/AI", "SC_7": "Economic",
};
const ttStyle = { background: "#1e293b", border: "1px solid #334155", borderRadius: 8, color: "white", fontSize: 12 };
const panelStyle = { background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.06)" };
// SC tooltip — shows full description on bar hover
const SCTooltip = ({ active, payload, label }) => {
if (!active || !payload || !payload.length) return null;
const scKey = scParagraphData.find(d => d.sc === label)?.scKey;
return (
<div style={{ ...ttStyle, padding: "12px 14px", minWidth: 280, maxWidth: 340 }}>
<div className="font-bold text-white text-sm mb-1">{label}</div>
{scKey && (
<div className="text-xs mb-3 leading-relaxed" style={{ color: scColorMap[scKey] }}>
{scFullText[scKey]}
</div>
)}
<div style={{ borderTop: "1px solid rgba(255,255,255,0.08)", paddingTop: 8 }}>
{payload.map((p, i) => (
<div key={i} className="flex justify-between gap-4 text-xs mb-1">
<span style={{ color: p.color }}>{p.name}</span>
<span className="font-bold text-white">{p.value} paragraphs</span>
</div>
))}
</div>
</div>
);
};
// Floating tooltip for NC rows
const NCHoverTooltip = ({ item, color }) => (
<div
className="absolute z-50 pointer-events-none rounded-lg shadow-xl"
style={{
...ttStyle,
padding: "10px 14px",
minWidth: 260,
maxWidth: 360,
bottom: "calc(100% + 8px)",
left: 0,
}}
>
<div className="font-bold text-sm mb-1" style={{ color }}>{item.nc}</div>
<div className="text-slate-300 text-xs leading-relaxed mb-2">{item.fullText}</div>
<div className="flex items-center gap-2 text-xs">
<span className="inline-block w-2 h-2 rounded-full" style={{ background: color }} />
<span style={{ color }}>{item.sc} — {scLabelMap[item.sc]}</span>
</div>
</div>
);
export default function Dashboard() {
const [activeRun, setActiveRun] = useState(0);
const [hoveredNC, setHoveredNC] = useState(null);
const visible = keywords.filter((k) => k.runs[activeRun] > 0);
const sorted = [...visible].sort((a, b) => b.runs[activeRun] - a.runs[activeRun]);
const cloudWords = sorted.map((k, i) => {
const weight = k.runs[activeRun];
const angle = i * 1.3 + 0.7;
const radius = 12 + i * 5.5;
return { ...k, weight, x: Math.max(8, Math.min(92, 50 + Math.cos(angle) * radius * 0.75)), y: Math.max(8, Math.min(92, 50 + Math.sin(angle) * radius * 0.5)) };
});
const ncBarData = topNCs[activeRun];
const maxNcCount = Math.max(...ncBarData.map(d => d.count));
return (
<div className="min-h-screen p-6" style={{ fontFamily: "'DM Sans', system-ui, sans-serif", background: "linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)" }}>
<div className="max-w-6xl mx-auto">
<div className="mb-5">
<h1 className="text-3xl font-black text-white tracking-tight">BERTopic Analysis Dashboard</h1>
<p className="text-sm text-slate-500 mt-1">Topic word maps, seed superclaim usage, and taxonomy growth across 5, 15, and 50 article runs</p>
</div>
{/* Run selector */}
<div className="flex gap-1 p-1 rounded-xl mb-5 w-fit" style={{ background: "rgba(255,255,255,0.05)" }}>
{runLabels.map((label, i) => (
<button key={i} onClick={() => setActiveRun(i)} className="px-6 py-3 rounded-lg text-sm font-bold transition-all" style={{ background: activeRun === i ? runColors[i] : "transparent", color: activeRun === i ? "white" : "#94a3b8", boxShadow: activeRun === i ? `0 4px 20px ${runColors[i]}40` : "none" }}>{label}</button>
))}
</div>
{/* Word cloud */}
<div className="relative rounded-2xl overflow-hidden mb-6" style={{ height: 440, background: "radial-gradient(ellipse at center, rgba(15,23,42,0.3) 0%, rgba(15,23,42,0.8) 100%)", border: "1px solid rgba(255,255,255,0.06)" }}>
<div className="absolute inset-0 opacity-5" style={{ backgroundImage: "linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)", backgroundSize: "40px 40px" }} />
{cloudWords.map((w, i) => {
const fontSize = Math.max(12, 12 + (w.weight / 100) * 34);
const opacity = 0.35 + (w.weight / 100) * 0.65;
const color = catColors[w.cat];
return (
<div key={i} className="absolute group" style={{ left: `${w.x}%`, top: `${w.y}%`, transform: "translate(-50%, -50%)", zIndex: Math.round(w.weight) }}>
<span className="font-black whitespace-nowrap cursor-default transition-all duration-500" style={{ fontSize: `${fontSize}px`, color, opacity, textShadow: w.weight > 60 ? `0 0 30px ${color}30` : "none" }}>{w.word}</span>
<div className="absolute left-1/2 -translate-x-1/2 bottom-full mb-2 hidden group-hover:block z-50 pointer-events-none">
<div className="rounded-lg px-3 py-2.5 text-xs shadow-xl" style={{ ...ttStyle, minWidth: 200 }}>
<div className="font-bold text-white text-sm mb-1">{w.word}</div>
<div className="mb-2" style={{ color }}>{catLabels[w.cat]}</div>
<div className="space-y-1.5">{runLabels.map((l, ri) => (
<div key={ri} className="flex items-center gap-2">
<span className="text-slate-500 w-16 text-xs">{l}</span>
<div className="flex-1 h-3 rounded-full overflow-hidden" style={{ background: "#0f172a" }}><div className="h-full rounded-full" style={{ width: `${w.runs[ri]}%`, background: ri === activeRun ? runColors[ri] : "#475569" }} /></div>
<span className="text-slate-400 w-6 text-right text-xs">{w.runs[ri]}</span>
</div>
))}</div>
</div>
</div>
</div>
);
})}
</div>
{/* Keyword rankings + narrative shifts */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
<div className="rounded-xl p-5" style={panelStyle}>
<h3 className="text-sm font-bold text-white mb-4">Top Keywords — <span style={{ color: runColors[activeRun] }}>{runLabels[activeRun]} Run</span></h3>
{sorted.slice(0, 10).map((k, i) => (
<div key={i} className="flex items-center gap-2 mb-2.5">
<span className="text-xs text-slate-600 w-4 text-right font-mono">{i + 1}</span>
<span className="text-sm font-semibold text-white w-28 truncate">{k.word}</span>
<div className="flex-1 flex items-center gap-1">{[0, 1, 2].map((ri) => (
<div key={ri} className="flex-1 h-4 rounded overflow-hidden" style={{ background: "#0f172a" }}><div className="h-full rounded transition-all duration-500" style={{ width: `${k.runs[ri]}%`, background: ri === activeRun ? runColors[ri] : "rgba(255,255,255,0.06)", opacity: ri === activeRun ? 1 : 0.4 }} /></div>
))}</div>
<div className="w-3 h-3 rounded-full shrink-0" style={{ background: catColors[k.cat] }} />
</div>
))}
<div className="flex gap-1 mt-3 pl-8">{runLabels.map((l, i) => (<div key={i} className="flex-1 text-center text-xs" style={{ color: runColors[i], opacity: i === activeRun ? 1 : 0.4 }}>{l.split("-")[0]}</div>))}</div>
</div>
<div className="rounded-xl p-5" style={panelStyle}>
<h3 className="text-sm font-bold text-white mb-4">Biggest Narrative Shifts</h3>
{[
{ word: "biofuels", dir: "down", note: "95 → 15 → 5 Dominant in small sample, irrelevant at scale" },
{ word: "technology", dir: "up", note: "30 → 50 → 95 Becomes the dominant narrative at scale" },
{ word: "monitoring", dir: "up", note: "0 → 0 → 70 Only visible with 50 articles" },
{ word: "offshore", dir: "up", note: "0 → 0 → 62 Oil infrastructure framing emerges at scale" },
{ word: "AI", dir: "up", note: "0 → 58 → 20 Peaked at 15 articles, prompted SC_6" },
{ word: "emissions", dir: "stable", note: "62 → 95 → 72 Always present, peaks at 15 articles" },
{ word: "algae", dir: "down", note: "55 → 5 → 0 Specific to the 5-article sample only" },
{ word: "sustainability", dir: "up", note: "20 → 65 → 50 Corporate green signalling grows" },
].map((item, i) => (
<div key={i} className="flex items-center gap-3 mb-2.5 py-1" style={{ borderBottom: "1px solid rgba(255,255,255,0.04)" }}>
<span className="text-lg w-5 text-center" style={{ color: item.dir === "up" ? "#4ade80" : item.dir === "down" ? "#f87171" : "#94a3b8" }}>{item.dir === "up" ? "↑" : item.dir === "down" ? "↓" : "→"}</span>
<span className="text-sm font-bold text-white w-28">{item.word}</span>
<span className="text-xs text-slate-500 flex-1">{item.note}</span>
</div>
))}
</div>
</div>
{/* SC paragraph counts + Top NC subclaims */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5 mt-5">
{/* SC paragraph bar chart — tooltip shows full SC description */}
<div className="rounded-xl p-5" style={panelStyle}>
<h3 className="text-sm font-bold text-white mb-1">Superclaim Paragraph Counts</h3>
<p className="text-xs text-slate-500 mb-1">Paragraphs mapped per superclaim category across runs</p>
<p className="text-xs mb-4" style={{ color: "rgba(148,163,184,0.5)" }}>Hover a bar to see the full superclaim</p>
<ResponsiveContainer width="100%" height={260}>
<BarChart data={scParagraphData} barCategoryGap="18%" margin={{ top: 4, right: 4, left: -10, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
<XAxis dataKey="sc" tick={{ fill: "#94a3b8", fontSize: 9 }} axisLine={{ stroke: "rgba(255,255,255,0.1)" }} interval={0} />
<YAxis tick={{ fill: "#64748b", fontSize: 10 }} axisLine={{ stroke: "rgba(255,255,255,0.1)" }} />
<Tooltip content={<SCTooltip />} />
<Legend wrapperStyle={{ color: "#94a3b8", fontSize: 11 }} />
<Bar dataKey="r5" name="5-Article" fill={runColors[0]} radius={[3,3,0,0]} opacity={activeRun === 0 ? 1 : 0.35} />
<Bar dataKey="r15" name="15-Article" fill={runColors[1]} radius={[3,3,0,0]} opacity={activeRun === 1 ? 1 : 0.35} />
<Bar dataKey="r50" name="50-Article" fill={runColors[2]} radius={[3,3,0,0]} opacity={activeRun === 2 ? 1 : 0.35} />
</BarChart>
</ResponsiveContainer>
<div className="flex flex-wrap gap-2 mt-3">
{Object.entries(scLabelMap).map(([sc, label]) => (
<div key={sc} className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full" style={{ background: scColorMap[sc] }} />
<span className="text-xs text-slate-500">{sc}: {label}</span>
</div>
))}
</div>
</div>
{/* Top 10 NCs — hover NC name to see full claim text */}
<div className="rounded-xl p-5" style={panelStyle}>
<h3 className="text-sm font-bold text-white mb-1">
Top 10 Subclaims — <span style={{ color: runColors[activeRun] }}>{runLabels[activeRun]} Run</span>
</h3>
<p className="text-xs text-slate-500 mb-1">Paragraph hits per subclaim (matched + created events)</p>
<p className="text-xs mb-4" style={{ color: "rgba(148,163,184,0.5)" }}>Hover an NC label to see the full subclaim</p>
<div className="space-y-2">
{ncBarData.map((item, i) => {
const pct = (item.count / maxNcCount) * 100;
const barColor = scColorMap[item.sc];
const isHovered = hoveredNC === `${activeRun}-${i}`;
return (
<div key={i} className="flex items-center gap-2">
<span className="text-xs text-slate-600 w-4 text-right font-mono">{i + 1}</span>
{/* NC label with hover tooltip */}
<div className="relative w-12 shrink-0">
<span
className="text-xs font-bold cursor-default"
style={{ color: isHovered ? "white" : barColor, transition: "color 0.15s" }}
onMouseEnter={() => setHoveredNC(`${activeRun}-${i}`)}
onMouseLeave={() => setHoveredNC(null)}
>
{item.nc}
</span>
{isHovered && <NCHoverTooltip item={item} color={barColor} />}
</div>
<div className="flex-1">
<div className="h-5 rounded overflow-hidden relative" style={{ background: "#0f172a" }}>
<div
className="h-full rounded flex items-center px-2 transition-all duration-500"
style={{ width: `${pct}%`, background: barColor, minWidth: 28 }}
>
<span className="text-xs font-bold text-white">{item.count}</span>
</div>
</div>
</div>
<span className="text-xs text-slate-500 w-36 truncate text-right">{item.label}</span>
</div>
);
})}
</div>
<div className="flex flex-wrap gap-x-3 gap-y-1 mt-3 text-xs text-slate-500">
{Object.entries(scLabelMap).map(([sc]) => (
<span key={sc} className="flex items-center gap-1">
<span className="inline-block w-2 h-2 rounded-full" style={{ background: scColorMap[sc] }} />
{sc}
</span>
))}
</div>
</div>
</div>
{/* Word map legend */}
<div className="flex flex-wrap gap-4 mt-5 pt-4" style={{ borderTop: "1px solid rgba(255,255,255,0.06)" }}>
{Object.entries(catLabels).map(([key, label]) => (
<div key={key} className="flex items-center gap-2"><div className="w-2.5 h-2.5 rounded-full" style={{ background: catColors[key] }} /><span className="text-xs text-slate-500">{label}</span></div>
))}
</div>
</div>
</div>
);
}