Files
liqiang b119135836
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
2026-08-20 13:12:50 +00:00

437 lines
13 KiB
Python

"""
Create animated bar chart race showing leaderboard evolution over time
"""
import pandas as pd
import numpy as np
from typing import List, Tuple
import json
import os
def prepare_animation_data(history_df: pd.DataFrame, top_n: int = 15) -> dict:
"""
Prepare data for D3.js bar chart race animation.
Args:
history_df: DataFrame with columns: date, model, rating, rank
top_n: Number of top models to show at each time point
Returns:
Dictionary with animation data
"""
# Convert date column to Timestamp to support ISO date strings and date objects
if history_df is not None and len(history_df) > 0 and 'date' in history_df.columns:
history_df = history_df.copy()
history_df['date'] = pd.to_datetime(history_df['date'])
# Get all unique dates
dates = sorted(history_df['date'].unique())
if len(dates) == 0:
return {
'frames': [],
'total_frames': 0,
'top_n': top_n,
'start_date': None,
'end_date': None,
}
# For each date, get top N models
frames = []
for date in dates:
date_data = history_df[history_df['date'] == date].nlargest(top_n, 'rating')
frame = {
'date': date.strftime('%Y-%m-%d'),
'timestamp': int(date.timestamp()),
'models': []
}
for rank, row in enumerate(date_data.itertuples(), 1):
frame['models'].append({
'rank': rank,
'name': row.model,
'rating': float(row.rating),
'matches': int(row.matches),
'wins': float(row.wins)
})
frames.append(frame)
animation_data = {
'frames': frames,
'total_frames': len(frames),
'top_n': top_n,
'start_date': dates[0].strftime('%Y-%m-%d'),
'end_date': dates[-1].strftime('%Y-%m-%d')
}
return animation_data
def generate_html_animation(animation_data: dict, output_path: str = "leaderboard_animation.html"):
"""
Generate standalone HTML file with D3.js bar chart race animation.
Args:
animation_data: Dictionary from prepare_animation_data
output_path: Path to save HTML file
"""
html_template = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Model Leaderboard Evolution</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
#container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
}
#date-display {
text-align: center;
font-size: 24px;
font-weight: bold;
color: #666;
margin-bottom: 20px;
}
#chart {
margin: 20px 0;
}
.bar {
fill: steelblue;
cursor: pointer;
transition: fill 0.3s;
}
.bar:hover {
fill: #4682b4;
}
.bar-label {
font-size: 14px;
fill: white;
font-weight: bold;
}
.bar-value {
font-size: 12px;
fill: #333;
}
.rank-label {
font-size: 18px;
fill: #666;
font-weight: bold;
}
#controls {
text-align: center;
margin-top: 30px;
}
button {
padding: 10px 20px;
margin: 0 5px;
font-size: 16px;
cursor: pointer;
border: none;
border-radius: 5px;
background: #4CAF50;
color: white;
transition: background 0.3s;
}
button:hover {
background: #45a049;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
#progress-bar {
width: 100%;
height: 5px;
background: #e0e0e0;
margin-top: 20px;
border-radius: 3px;
overflow: hidden;
}
#progress {
height: 100%;
background: #4CAF50;
width: 0%;
transition: width 0.5s;
}
#speed-control {
margin-top: 20px;
text-align: center;
}
#speed-slider {
width: 300px;
margin: 0 10px;
}
.info-box {
background: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div id="container">
<h1>🏆 Model Leaderboard Evolution</h1>
<div id="date-display">Loading...</div>
<div id="chart"></div>
<div id="controls">
<button id="play-btn">▶ Play</button>
<button id="pause-btn" disabled>⏸ Pause</button>
<button id="reset-btn">↺ Reset</button>
</div>
<div id="progress-bar">
<div id="progress"></div>
</div>
<div id="speed-control">
<label>Speed: </label>
<input type="range" id="speed-slider" min="1" max="10" value="5">
<span id="speed-value">5x</span>
</div>
<div class="info-box">
<strong>About:</strong> This animation shows the evolution of model rankings based on Elo ratings
calculated from Chatbot Arena voting data. Each frame represents a snapshot in time,
with models ranked by their current Elo rating. Bars show the rating value,
and the animation reveals how models compete and evolve over time.
</div>
</div>
<script>
const data = """ + json.dumps(animation_data, indent=2) + """;
// Configuration
const margin = {top: 20, right: 100, bottom: 40, left: 50};
const width = 1100 - margin.left - margin.right;
const height = 600 - margin.top - margin.bottom;
const barHeight = height / data.top_n - 5;
// Create SVG
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Scales
const xScale = d3.scaleLinear()
.domain([0, d3.max(data.frames.flatMap(f => f.models.map(m => m.rating)))])
.range([0, width - 200]);
// Color scale
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
// Animation state
let currentFrame = 0;
let isPlaying = false;
let animationInterval = null;
let animationSpeed = 500; // milliseconds per frame
// Update speed based on slider
d3.select("#speed-slider").on("input", function() {
const speed = +this.value;
animationSpeed = 1000 / speed;
d3.select("#speed-value").text(`${speed}x`);
if (isPlaying) {
stopAnimation();
startAnimation();
}
});
function updateChart(frameIndex) {
const frame = data.frames[frameIndex];
// Update date display
d3.select("#date-display").text(frame.date);
// Update progress bar
const progress = ((frameIndex + 1) / data.total_frames) * 100;
d3.select("#progress").style("width", `${progress}%`);
// Update max value for scale
const maxRating = d3.max(frame.models, d => d.rating);
xScale.domain([0, maxRating * 1.1]);
// Bind data
const bars = svg.selectAll(".bar-group")
.data(frame.models, d => d.name);
// Remove old bars
bars.exit()
.transition()
.duration(animationSpeed * 0.8)
.style("opacity", 0)
.remove();
// Add new bars
const enter = bars.enter()
.append("g")
.attr("class", "bar-group")
.style("opacity", 0);
enter.append("rect")
.attr("class", "bar")
.attr("height", barHeight);
enter.append("text")
.attr("class", "bar-label")
.attr("x", 10)
.attr("y", barHeight / 2)
.attr("dy", "0.35em");
enter.append("text")
.attr("class", "bar-value")
.attr("y", barHeight / 2)
.attr("dy", "0.35em");
enter.append("text")
.attr("class", "rank-label")
.attr("x", -40)
.attr("y", barHeight / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle");
// Update all bars
const merged = enter.merge(bars);
merged.transition()
.duration(animationSpeed * 0.8)
.style("opacity", 1)
.attr("transform", (d, i) => `translate(0,${i * (barHeight + 5)})`);
merged.select(".bar")
.transition()
.duration(animationSpeed * 0.8)
.attr("width", d => xScale(d.rating))
.attr("fill", d => colorScale(d.name));
merged.select(".bar-label")
.text(d => d.name);
merged.select(".bar-value")
.transition()
.duration(animationSpeed * 0.8)
.attr("x", d => xScale(d.rating) + 10)
.text(d => `${Math.round(d.rating)} (${d.matches} matches)`);
merged.select(".rank-label")
.text(d => `#${d.rank}`);
}
function startAnimation() {
if (currentFrame >= data.total_frames - 1) {
currentFrame = 0;
}
isPlaying = true;
d3.select("#play-btn").property("disabled", true);
d3.select("#pause-btn").property("disabled", false);
animationInterval = setInterval(() => {
updateChart(currentFrame);
currentFrame++;
if (currentFrame >= data.total_frames) {
stopAnimation();
currentFrame = data.total_frames - 1;
}
}, animationSpeed);
}
function stopAnimation() {
isPlaying = false;
d3.select("#play-btn").property("disabled", false);
d3.select("#pause-btn").property("disabled", true);
if (animationInterval) {
clearInterval(animationInterval);
animationInterval = null;
}
}
function resetAnimation() {
stopAnimation();
currentFrame = 0;
updateChart(currentFrame);
d3.select("#progress").style("width", "0%");
}
// Button handlers
d3.select("#play-btn").on("click", startAnimation);
d3.select("#pause-btn").on("click", stopAnimation);
d3.select("#reset-btn").on("click", resetAnimation);
// Initialize with first frame
updateChart(0);
// Auto-play on load
setTimeout(startAnimation, 1000);
</script>
</body>
</html>"""
# Keep generated evidence friendly to `git diff --check` and deterministic
# across editors that otherwise strip indentation-only lines.
html_template = "\n".join(line.rstrip() for line in html_template.splitlines()) + "\n"
# Write to file
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_template)
print(f"Generated animation HTML at: {output_path}")
print(f"Open the file in a web browser to view the animation.")
def create_simple_animation(history_df: pd.DataFrame, output_path: str = "leaderboard_animation.html", top_n: int = 15):
"""
Convenience function to create animation in one step.
Args:
history_df: DataFrame with rating history
output_path: Path to save HTML file
top_n: Number of top models to show
"""
print("Preparing animation data...")
animation_data = prepare_animation_data(history_df, top_n)
print(f"Generating HTML animation with {animation_data['total_frames']} frames...")
generate_html_animation(animation_data, output_path)
return output_path