Architecture designer, author cluster names, FP filtering, new pages
- Add /architecture page: system-of-systems view with 8 layers, component cards, gap markers, source coverage chart, and clickable detail sidebar - Give author clusters meaningful names from orgs + draft topic keywords - Filter false positives (73 drafts, 54 ideas) from idea clusters, architecture, ideas listing, and search results - Add NIST source fetcher with curated catalog of 11 AI publications - New pages: trends, complexity, sources, false positives, idea analysis - Clickable gap cards with full details (evidence, priority, nearby work) - Component detail panel with linked drafts and top ideas Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "citations" %}
|
||||
|
||||
{% block title %}Citation Graph — IETF Draft Analyzer{% endblock %}
|
||||
{% block title %}Citations & Influence — IETF Draft Analyzer{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="/static/js/d3.v7.min.js"></script>
|
||||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||||
<style>
|
||||
#citationSvg {
|
||||
width: 100%;
|
||||
@@ -26,95 +27,368 @@
|
||||
max-width: 320px; opacity: 0; transition: opacity 0.15s;
|
||||
}
|
||||
.tooltip-card.visible { opacity: 1; }
|
||||
.legend-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; }
|
||||
.filter-btn { transition: all 0.15s; }
|
||||
.filter-btn:hover { background: rgba(59, 130, 246, 0.2); }
|
||||
.filter-btn.active { background: rgba(59, 130, 246, 0.3); border-color: #3b82f6; color: #60a5fa; }
|
||||
.tab-btn {
|
||||
transition: all 0.15s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.tab-btn:hover { color: #e2e8f0; }
|
||||
.tab-btn.active {
|
||||
color: #60a5fa;
|
||||
border-bottom-color: #3b82f6;
|
||||
}
|
||||
.tab-panel { display: none; }
|
||||
.tab-panel.active { display: block; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-white">Citation Graph</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Cross-reference network: {{ graph.stats.draft_count }} drafts referencing {{ graph.stats.rfc_count }} RFCs. References are extracted from each draft's text (RFC mentions, draft citations, BCP references). Node size reflects influence — how many other documents cite it. Highly-cited RFCs represent foundational standards that AI/agent drafts build upon.</p>
|
||||
<h1 class="text-2xl font-bold text-white">Citations & Influence</h1>
|
||||
<p class="text-slate-400 text-sm mt-1">Cross-reference network, citation influence metrics, and BCP dependency analysis across {{ influence.stats.drafts_with_refs }} drafts and {{ influence.stats.total_citations }} total citations.</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.draft_count }}</div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Citations</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.total_citations }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-500 to-orange-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Referenced RFCs</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.rfc_count }}</div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Unique RFCs Cited</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.unique_rfcs }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Nodes</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.node_count }}</div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts with Refs</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.drafts_with_refs }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Citation Links</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.edge_count }}</div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Refs/Draft</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ influence.stats.avg_refs_per_draft }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">BCP Coverage</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- D3 Force-directed Citation Graph -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6 relative">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-slate-300">Cross-Reference Network</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 align-middle mr-1"></span>Drafts
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-orange-500 align-middle mr-1 ml-3"></span>RFCs
|
||||
— Node size = influence (in-degree). Drag to rearrange. Scroll to zoom.
|
||||
</p>
|
||||
<!-- Tabs -->
|
||||
<div class="border-b border-slate-800 mb-6">
|
||||
<nav class="flex gap-6">
|
||||
<button class="tab-btn active px-1 pb-3 text-sm font-medium text-slate-400" data-tab="graph">Citation Graph</button>
|
||||
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="influence">Influence Analysis</button>
|
||||
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="bcp">BCP Dependencies</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- ==================== TAB 1: Citation Graph ==================== -->
|
||||
<div id="tab-graph" class="tab-panel active">
|
||||
<!-- D3 Force-directed Citation Graph -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6 relative">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-slate-300">Cross-Reference Network</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 align-middle mr-1"></span>Drafts
|
||||
<span class="inline-block w-2.5 h-2.5 rounded-full bg-orange-500 align-middle mr-1 ml-3"></span>RFCs
|
||||
— Node size = influence. Drag to rearrange. Scroll to zoom.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<button id="resetZoom" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 transition">Reset View</button>
|
||||
<select id="filterCategory" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 bg-slate-800 text-slate-300 focus:outline-none focus:border-blue-500">
|
||||
<option value="">All Categories</option>
|
||||
</select>
|
||||
<label class="text-xs text-slate-500 ml-2">Min refs:</label>
|
||||
<input type="range" id="minRefsSlider" min="1" max="15" value="2" class="w-20" style="accent-color: #3b82f6;">
|
||||
<span id="minRefsVal" class="text-xs text-blue-400 font-mono w-4">2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<button id="resetZoom" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 text-slate-400 hover:text-white hover:border-slate-500 transition">Reset View</button>
|
||||
<select id="filterCategory" class="text-xs px-3 py-1.5 rounded-lg border border-slate-700 bg-slate-800 text-slate-300 focus:outline-none focus:border-blue-500">
|
||||
<option value="">All Categories</option>
|
||||
</select>
|
||||
<label class="text-xs text-slate-500 ml-2">Min refs:</label>
|
||||
<input type="range" id="minRefsSlider" min="1" max="15" value="2" class="w-20" style="accent-color: #3b82f6;">
|
||||
<span id="minRefsVal" class="text-xs text-blue-400 font-mono w-4">2</span>
|
||||
<div class="relative">
|
||||
<svg id="citationSvg"></svg>
|
||||
<div id="tooltip" class="tooltip-card"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative">
|
||||
<svg id="citationSvg"></svg>
|
||||
<div id="tooltip" class="tooltip-card"></div>
|
||||
|
||||
<!-- Top Referenced RFCs Table -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">Most Referenced RFCs</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">RFCs cited by the most drafts in the corpus</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table class="w-full text-sm" id="rfcTable">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50" id="rfcBody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Referenced RFCs Table -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">Most Referenced RFCs</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">RFCs cited by the most drafts in the corpus</p>
|
||||
<!-- ==================== TAB 2: Influence Analysis ==================== -->
|
||||
<div id="tab-influence" class="tab-panel">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- Top Cited RFCs with details -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most-Cited RFCs</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Foundational standards the AI/agent ecosystem builds upon</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Name</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
{% for rfc in influence.top_cited_rfcs %}
|
||||
<tr class="hover:bg-slate-800/50 transition group">
|
||||
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<a href="https://www.rfc-editor.org/rfc/rfc{{ rfc.rfc_id }}" target="_blank" rel="noopener"
|
||||
class="text-orange-400 hover:text-orange-300 transition font-medium text-sm">RFC {{ rfc.rfc_id }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-slate-300 text-xs">{{ rfc.name }}</td>
|
||||
<td class="px-4 py-2.5 text-right">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-medium
|
||||
{% if rfc.count >= 20 %}bg-orange-500/20 text-orange-400
|
||||
{% elif rfc.count >= 10 %}bg-blue-500/20 text-blue-400
|
||||
{% else %}bg-slate-700/50 text-slate-400{% endif %}">
|
||||
{{ rfc.count }} drafts
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Citing Drafts -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most-Citing Drafts</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Drafts with the highest outgoing reference count</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Draft</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Refs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
{% for d in influence.top_citing_drafts %}
|
||||
<tr class="hover:bg-slate-800/50 transition">
|
||||
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-sm">
|
||||
{{ d.title[:60] }}{% if d.title|length > 60 %}...{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-700/50 text-slate-300">{{ d.category }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-right">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-500/20 text-blue-400">{{ d.count }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table class="w-full text-sm" id="rfcTable">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50" id="rfcBody">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- PageRank-style influence -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">Influence Score (PageRank-style)</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Drafts ranked by weighted sum of how often their cited RFCs are themselves cited — higher score means citing more foundational standards</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[400px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Draft</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Out-Degree</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Influence Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
{% for d in influence.top_pagerank %}
|
||||
<tr class="hover:bg-slate-800/50 transition">
|
||||
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-sm">
|
||||
{{ d.title[:60] }}{% if d.title|length > 60 %}...{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-2.5">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-700/50 text-slate-300">{{ d.category }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-400">{{ d.out_degree }}</td>
|
||||
<td class="px-4 py-2.5 text-right">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-500/20 text-purple-400">{{ d.score }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Citation density by category chart -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Average Citations per Category</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">Which categories reference the most external standards</p>
|
||||
<div id="categoryChart" style="height: 400px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Draft-to-Draft network -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">Draft-to-Draft Citation Network</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">{{ influence.draft_network|length }} cross-citations between drafts in the corpus</p>
|
||||
<div id="draftNetworkChart" style="height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== TAB 3: BCP Dependencies ==================== -->
|
||||
<div id="tab-bcp" class="tab-panel">
|
||||
<!-- BCP Stats -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Unique BCPs</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.unique_bcps }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Total BCP Refs</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.total_bcp_refs }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts with BCPs</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.drafts_with_bcp }}</div>
|
||||
</div>
|
||||
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
|
||||
<div class="text-xs text-slate-500 uppercase tracking-wider">BCP Coverage</div>
|
||||
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<!-- BCP Citation Table -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">All BCPs by Citation Count</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">{{ bcp.coverage.unique_bcps }} unique BCPs cited across the corpus</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">BCP</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Example Drafts</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
{% for b in bcp.bcps %}
|
||||
<tr class="hover:bg-slate-800/50 transition">
|
||||
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
|
||||
<td class="px-4 py-2.5 font-medium text-amber-400">BCP {{ b.bcp_id }}</td>
|
||||
<td class="px-4 py-2.5 text-right">
|
||||
<span class="px-2 py-0.5 rounded-full text-xs font-medium
|
||||
{% if b.count >= 50 %}bg-amber-500/20 text-amber-400
|
||||
{% elif b.count >= 10 %}bg-blue-500/20 text-blue-400
|
||||
{% else %}bg-slate-700/50 text-slate-400{% endif %}">
|
||||
{{ b.count }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-2.5 text-xs text-slate-500 max-w-[200px] truncate">
|
||||
{{ b.drafts[:3]|join(', ') }}{% if b.total_drafts > 3 %} +{{ b.total_drafts - 3 }} more{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BCP by Category -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<div class="p-4 border-b border-slate-800">
|
||||
<h2 class="text-sm font-semibold text-slate-300">BCP Usage by Category</h2>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Which categories rely most heavily on BCPs</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="sticky top-0 z-10">
|
||||
<tr class="border-b border-slate-800 bg-slate-900">
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">BCP Refs</th>
|
||||
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Unique BCPs</th>
|
||||
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Top BCPs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-800/50">
|
||||
{% for cat in bcp.by_category %}
|
||||
<tr class="hover:bg-slate-800/50 transition">
|
||||
<td class="px-4 py-2.5 text-slate-300 text-sm font-medium">{{ cat.category }}</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-300">{{ cat.total_bcp_refs }}</td>
|
||||
<td class="px-4 py-2.5 text-right text-slate-400">{{ cat.unique_bcps }}</td>
|
||||
<td class="px-4 py-2.5 text-xs text-slate-500">
|
||||
{% for tb in cat.top_bcps[:3] %}
|
||||
<span class="inline-block px-1.5 py-0.5 rounded bg-slate-700/50 text-amber-400 mr-1 mb-0.5">BCP{{ tb.bcp_id }}({{ tb.count }})</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BCP Co-citation Heatmap -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
|
||||
<h2 class="text-sm font-semibold text-slate-300 mb-1">BCP Co-Citation Heatmap</h2>
|
||||
<p class="text-xs text-slate-500 mb-3">How often pairs of BCPs are cited together in the same draft. Darker = more co-citations.</p>
|
||||
<div id="bcpHeatmap" style="height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<script>
|
||||
const graph = {{ graph | tojson }};
|
||||
const influence = {{ influence | tojson }};
|
||||
const bcp = {{ bcp | tojson }};
|
||||
|
||||
const PALETTE = [
|
||||
'#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b',
|
||||
@@ -122,7 +396,22 @@ const PALETTE = [
|
||||
];
|
||||
|
||||
// ===========================================================
|
||||
// D3.js Force-Directed Citation Network
|
||||
// Tab switching
|
||||
// ===========================================================
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
|
||||
|
||||
// Trigger Plotly resize for charts that might not have rendered
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================
|
||||
// D3.js Force-Directed Citation Network (Tab 1)
|
||||
// ===========================================================
|
||||
(function() {
|
||||
if (graph.nodes.length === 0) {
|
||||
@@ -184,27 +473,20 @@ const PALETTE = [
|
||||
const maxInfluence = d3.max(nodes, n => n.influence) || 1;
|
||||
const rScale = d3.scaleSqrt().domain([0, maxInfluence]).range([3, 24]);
|
||||
|
||||
// Color: drafts = blue, rfcs = orange, others = amber
|
||||
function nodeColor(n) {
|
||||
if (n.type === 'rfc') return '#f59e0b';
|
||||
if (n.type === 'bcp') return '#eab308';
|
||||
return '#3b82f6';
|
||||
}
|
||||
|
||||
// Force simulation
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links)
|
||||
.id(d => d.id)
|
||||
.distance(60)
|
||||
.strength(0.15)
|
||||
)
|
||||
.force('link', d3.forceLink(links).id(d => d.id).distance(60).strength(0.15))
|
||||
.force('charge', d3.forceManyBody().strength(-80).distanceMax(350))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collision', d3.forceCollide().radius(d => rScale(d.influence) + 2))
|
||||
.force('x', d3.forceX(width / 2).strength(0.04))
|
||||
.force('y', d3.forceY(height / 2).strength(0.04));
|
||||
|
||||
// Zoom behavior
|
||||
const g = svg.append('g');
|
||||
const zoom = d3.zoom()
|
||||
.scaleExtent([0.15, 5])
|
||||
@@ -215,33 +497,22 @@ const PALETTE = [
|
||||
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
|
||||
});
|
||||
|
||||
// Draw edges
|
||||
const linkGroup = g.append('g').attr('class', 'links');
|
||||
const link = linkGroup.selectAll('line')
|
||||
.data(links)
|
||||
.join('line')
|
||||
.attr('class', 'link')
|
||||
.attr('stroke', '#475569')
|
||||
.attr('stroke-width', 0.8);
|
||||
.data(links).join('line')
|
||||
.attr('class', 'link').attr('stroke', '#475569').attr('stroke-width', 0.8);
|
||||
|
||||
// Draw nodes
|
||||
const nodeGroup = g.append('g').attr('class', 'nodes');
|
||||
const node = nodeGroup.selectAll('g')
|
||||
.data(nodes)
|
||||
.join('g')
|
||||
.attr('class', 'node')
|
||||
.data(nodes).join('g').attr('class', 'node')
|
||||
.call(d3.drag()
|
||||
.on('start', dragStarted)
|
||||
.on('drag', dragged)
|
||||
.on('end', dragEnded)
|
||||
);
|
||||
.on('start', dragStarted).on('drag', dragged).on('end', dragEnded));
|
||||
|
||||
node.append('circle')
|
||||
.attr('r', d => rScale(d.influence))
|
||||
.attr('fill', d => nodeColor(d))
|
||||
.attr('opacity', 0.85);
|
||||
|
||||
// Labels for high-influence nodes
|
||||
node.filter(d => d.influence >= 5)
|
||||
.append('text')
|
||||
.text(d => {
|
||||
@@ -251,11 +522,9 @@ const PALETTE = [
|
||||
})
|
||||
.attr('dy', d => -(rScale(d.influence) + 4))
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', '#94a3b8')
|
||||
.attr('font-size', '8px')
|
||||
.attr('fill', '#94a3b8').attr('font-size', '8px')
|
||||
.attr('font-family', 'Inter, system-ui, sans-serif');
|
||||
|
||||
// Tooltip
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
|
||||
node.on('mouseover', function(event, d) {
|
||||
@@ -270,8 +539,6 @@ const PALETTE = [
|
||||
</div>
|
||||
`;
|
||||
tooltip.classList.add('visible');
|
||||
|
||||
// Highlight connected nodes
|
||||
const connected = new Set();
|
||||
links.forEach(l => {
|
||||
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
@@ -280,17 +547,13 @@ const PALETTE = [
|
||||
if (tid === d.id) connected.add(sid);
|
||||
});
|
||||
connected.add(d.id);
|
||||
|
||||
node.select('circle')
|
||||
.attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
|
||||
node.selectAll('text')
|
||||
.attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
|
||||
link
|
||||
.attr('stroke-opacity', l => {
|
||||
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
return (sid === d.id || tid === d.id) ? 0.6 : 0.02;
|
||||
});
|
||||
node.select('circle').attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
|
||||
node.selectAll('text').attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
|
||||
link.attr('stroke-opacity', l => {
|
||||
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
return (sid === d.id || tid === d.id) ? 0.6 : 0.02;
|
||||
});
|
||||
})
|
||||
.on('mousemove', function(event) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
@@ -311,30 +574,22 @@ const PALETTE = [
|
||||
}
|
||||
});
|
||||
|
||||
// Tick handler
|
||||
simulation.on('tick', () => {
|
||||
link
|
||||
.attr('x1', d => d.source.x)
|
||||
.attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x)
|
||||
.attr('y2', d => d.target.y);
|
||||
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
||||
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Drag handlers
|
||||
function dragStarted(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
d.fx = d.x; d.fy = d.y;
|
||||
}
|
||||
function dragged(event, d) {
|
||||
d.fx = event.x; d.fy = event.y;
|
||||
}
|
||||
function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
|
||||
function dragEnded(event, d) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
d.fx = null; d.fy = null;
|
||||
}
|
||||
|
||||
// Category filter
|
||||
catSelect.addEventListener('change', function() {
|
||||
const cat = this.value;
|
||||
if (!cat) {
|
||||
@@ -344,43 +599,33 @@ const PALETTE = [
|
||||
return;
|
||||
}
|
||||
const inCat = new Set();
|
||||
nodes.forEach(n => {
|
||||
if (n.type === 'draft' && n.category === cat) inCat.add(n.id);
|
||||
});
|
||||
// Also include RFCs referenced by those drafts
|
||||
nodes.forEach(n => { if (n.type === 'draft' && n.category === cat) inCat.add(n.id); });
|
||||
links.forEach(l => {
|
||||
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
if (inCat.has(sid)) inCat.add(tid);
|
||||
});
|
||||
node.select('circle')
|
||||
.attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
|
||||
node.selectAll('text')
|
||||
.attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
|
||||
node.select('circle').attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
|
||||
node.selectAll('text').attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
|
||||
link.attr('stroke-opacity', l => {
|
||||
const sid = typeof l.source === 'object' ? l.source.id : l.source;
|
||||
return inCat.has(sid) ? 0.5 : 0.01;
|
||||
});
|
||||
});
|
||||
|
||||
// Min refs slider (client-side filter)
|
||||
const slider = document.getElementById('minRefsSlider');
|
||||
const sliderVal = document.getElementById('minRefsVal');
|
||||
slider.addEventListener('input', function() {
|
||||
sliderVal.textContent = this.value;
|
||||
const minR = parseInt(this.value);
|
||||
// Show/hide RFC nodes by influence
|
||||
node.select('circle')
|
||||
.attr('opacity', n => {
|
||||
if (n.type === 'draft') return 0.85;
|
||||
return n.influence >= minR ? 0.85 : 0.05;
|
||||
});
|
||||
node.selectAll('text')
|
||||
.attr('opacity', n => {
|
||||
if (n.type === 'draft') return 1;
|
||||
return n.influence >= minR ? 1 : 0.05;
|
||||
});
|
||||
// Filter edges
|
||||
node.select('circle').attr('opacity', n => {
|
||||
if (n.type === 'draft') return 0.85;
|
||||
return n.influence >= minR ? 0.85 : 0.05;
|
||||
});
|
||||
node.selectAll('text').attr('opacity', n => {
|
||||
if (n.type === 'draft') return 1;
|
||||
return n.influence >= minR ? 1 : 0.05;
|
||||
});
|
||||
const visibleRfcs = new Set(nodes.filter(n => n.type !== 'draft' && n.influence >= minR).map(n => n.id));
|
||||
link.attr('stroke-opacity', l => {
|
||||
const tid = typeof l.target === 'object' ? l.target.id : l.target;
|
||||
@@ -388,5 +633,155 @@ const PALETTE = [
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
// ===========================================================
|
||||
// Plotly: Citation density by category (Tab 2)
|
||||
// ===========================================================
|
||||
(function() {
|
||||
const cats = influence.citations_by_category;
|
||||
if (!cats || cats.length === 0) return;
|
||||
|
||||
Plotly.newPlot('categoryChart', [{
|
||||
type: 'bar',
|
||||
x: cats.map(c => c.category),
|
||||
y: cats.map(c => c.avg_citations),
|
||||
text: cats.map(c => `${c.avg_citations} avg (${c.total_citations} total, ${c.draft_count} drafts)`),
|
||||
hovertemplate: '%{x}<br>Avg: %{y:.1f} refs/draft<br>%{text}<extra></extra>',
|
||||
marker: {
|
||||
color: cats.map((_, i) => PALETTE[i % PALETTE.length]),
|
||||
opacity: 0.85,
|
||||
},
|
||||
}], {
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 11 },
|
||||
margin: { t: 20, r: 20, b: 80, l: 50 },
|
||||
xaxis: {
|
||||
tickangle: -35,
|
||||
gridcolor: 'rgba(71,85,105,0.2)',
|
||||
},
|
||||
yaxis: {
|
||||
title: 'Avg Citations per Draft',
|
||||
gridcolor: 'rgba(71,85,105,0.2)',
|
||||
},
|
||||
}, { responsive: true, displayModeBar: false });
|
||||
})();
|
||||
|
||||
// ===========================================================
|
||||
// Plotly: Draft-to-Draft Network (Tab 2)
|
||||
// ===========================================================
|
||||
(function() {
|
||||
const edges = influence.draft_network;
|
||||
if (!edges || edges.length === 0) {
|
||||
document.getElementById('draftNetworkChart').innerHTML =
|
||||
'<p class="text-slate-500 text-sm text-center py-20">No draft-to-draft citations found</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a simple force-layout-like visualization using Plotly scatter
|
||||
// We'll use a circular layout for nodes involved in draft-to-draft citations
|
||||
const nodeSet = new Set();
|
||||
edges.forEach(e => { nodeSet.add(e.source); nodeSet.add(e.target); });
|
||||
const nodeList = [...nodeSet];
|
||||
const nodeIdx = Object.fromEntries(nodeList.map((n, i) => [n, i]));
|
||||
|
||||
// Circular layout
|
||||
const n = nodeList.length;
|
||||
const cx = 0.5, cy = 0.5, radius = 0.4;
|
||||
const nodeX = nodeList.map((_, i) => cx + radius * Math.cos(2 * Math.PI * i / n));
|
||||
const nodeY = nodeList.map((_, i) => cy + radius * Math.sin(2 * Math.PI * i / n));
|
||||
|
||||
// Edge traces
|
||||
const edgeX = [], edgeY = [];
|
||||
edges.forEach(e => {
|
||||
const si = nodeIdx[e.source], ti = nodeIdx[e.target];
|
||||
if (si !== undefined && ti !== undefined) {
|
||||
edgeX.push(nodeX[si], nodeX[ti], null);
|
||||
edgeY.push(nodeY[si], nodeY[ti], null);
|
||||
}
|
||||
});
|
||||
|
||||
// Short names for display
|
||||
const shortNames = nodeList.map(name => {
|
||||
const s = name.replace(/^draft-/, '');
|
||||
return s.length > 25 ? s.slice(0, 23) + '..' : s;
|
||||
});
|
||||
|
||||
const data = [
|
||||
{
|
||||
type: 'scatter', mode: 'lines',
|
||||
x: edgeX, y: edgeY,
|
||||
line: { color: 'rgba(71,85,105,0.3)', width: 1 },
|
||||
hoverinfo: 'none',
|
||||
},
|
||||
{
|
||||
type: 'scatter', mode: 'markers+text',
|
||||
x: nodeX, y: nodeY,
|
||||
text: shortNames,
|
||||
textposition: 'top center',
|
||||
textfont: { size: 8, color: '#94a3b8' },
|
||||
marker: { size: 8, color: '#3b82f6', opacity: 0.85 },
|
||||
hovertext: nodeList.map(name => {
|
||||
const outCount = edges.filter(e => e.source === name).length;
|
||||
const inCount = edges.filter(e => e.target === name).length;
|
||||
return `${name}\nOut: ${outCount} | In: ${inCount}`;
|
||||
}),
|
||||
hoverinfo: 'text',
|
||||
},
|
||||
];
|
||||
|
||||
Plotly.newPlot('draftNetworkChart', data, {
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif' },
|
||||
margin: { t: 10, r: 10, b: 10, l: 10 },
|
||||
xaxis: { visible: false },
|
||||
yaxis: { visible: false },
|
||||
showlegend: false,
|
||||
}, { responsive: true, displayModeBar: false });
|
||||
})();
|
||||
|
||||
// ===========================================================
|
||||
// Plotly: BCP Co-citation Heatmap (Tab 3)
|
||||
// ===========================================================
|
||||
(function() {
|
||||
const labels = bcp.heatmap_labels;
|
||||
const matrix = bcp.heatmap_matrix;
|
||||
if (!labels || labels.length === 0) {
|
||||
document.getElementById('bcpHeatmap').innerHTML =
|
||||
'<p class="text-slate-500 text-sm text-center py-20">No BCP co-citation data</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const displayLabels = labels.map(l => 'BCP ' + l);
|
||||
|
||||
Plotly.newPlot('bcpHeatmap', [{
|
||||
type: 'heatmap',
|
||||
x: displayLabels,
|
||||
y: displayLabels,
|
||||
z: matrix,
|
||||
colorscale: [
|
||||
[0, '#0f172a'],
|
||||
[0.1, '#1e3a5f'],
|
||||
[0.3, '#1d4ed8'],
|
||||
[0.5, '#3b82f6'],
|
||||
[0.7, '#60a5fa'],
|
||||
[1, '#f59e0b'],
|
||||
],
|
||||
hovertemplate: '%{x} + %{y}<br>Co-cited in %{z} drafts<extra></extra>',
|
||||
showscale: true,
|
||||
colorbar: {
|
||||
title: { text: 'Co-citations', font: { color: '#94a3b8', size: 10 } },
|
||||
tickfont: { color: '#94a3b8', size: 9 },
|
||||
},
|
||||
}], {
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 10 },
|
||||
margin: { t: 20, r: 60, b: 80, l: 80 },
|
||||
xaxis: { tickangle: -45, side: 'bottom' },
|
||||
yaxis: { autorange: 'reversed' },
|
||||
}, { responsive: true, displayModeBar: false });
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user