Skip to content

Commit 4fd8dd0

Browse files
committed
feat: Add sorting functionality to merbench leaderboard table
- Implement clickable column headers with sort indicators, sorting utilities, and state management. - Headers now support ascending/descending sort by model name, success rate, cost, duration, tokens, runs, and provider with visual feedback.
1 parent 0fdf50a commit 4fd8dd0

File tree

5 files changed

+253
-15
lines changed

5 files changed

+253
-15
lines changed

src/components/merbench/LeaderboardTable.astro

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,36 @@ const costRange = maxCost - minCost;
1818
<section class="leaderboard-section">
1919
<h2>Model Leaderboard</h2>
2020
<div class="leaderboard-table">
21-
<table>
21+
<table id="leaderboard-table">
2222
<thead>
2323
<tr>
2424
<th>Rank</th>
25-
<th>Model</th>
26-
<th>Success Rate</th>
27-
<th>Avg Cost/Run</th>
28-
<th>Avg Duration</th>
29-
<th>Avg Tokens</th>
30-
<th>Runs</th>
31-
<th>Provider</th>
25+
<th class="sortable" data-sort-key="Model" data-sort-type="string">
26+
Model <span class="sort-indicator"></span>
27+
</th>
28+
<th
29+
class="sortable active"
30+
data-sort-key="Success_Rate"
31+
data-sort-type="number"
32+
data-sort-direction="desc"
33+
>
34+
Success Rate <span class="sort-indicator">↓</span>
35+
</th>
36+
<th class="sortable" data-sort-key="Avg_Cost" data-sort-type="number">
37+
Avg Cost/Run <span class="sort-indicator"></span>
38+
</th>
39+
<th class="sortable" data-sort-key="Avg_Duration" data-sort-type="number">
40+
Avg Duration <span class="sort-indicator"></span>
41+
</th>
42+
<th class="sortable" data-sort-key="Avg_Tokens" data-sort-type="number">
43+
Avg Tokens <span class="sort-indicator"></span>
44+
</th>
45+
<th class="sortable" data-sort-key="Runs" data-sort-type="number">
46+
Runs <span class="sort-indicator"></span>
47+
</th>
48+
<th class="sortable" data-sort-key="Provider" data-sort-type="string">
49+
Provider <span class="sort-indicator"></span>
50+
</th>
3251
</tr>
3352
</thead>
3453
<tbody>
@@ -179,6 +198,42 @@ const costRange = maxCost - minCost;
179198
letter-spacing: 0.5px;
180199
}
181200

201+
/* Sortable header styles */
202+
.sortable {
203+
cursor: pointer;
204+
user-select: none;
205+
position: relative;
206+
transition: background-color 0.2s ease;
207+
}
208+
209+
.sortable:hover {
210+
background-color: var(--bg-tertiary);
211+
color: var(--text-primary);
212+
}
213+
214+
.sortable.active {
215+
/* No special styling - only the arrow indicator shows active state */
216+
}
217+
218+
.sort-indicator {
219+
font-size: 0.8rem;
220+
opacity: 0.5;
221+
margin-left: 0.25rem;
222+
}
223+
224+
.sortable.active .sort-indicator {
225+
opacity: 1;
226+
color: var(--accent-primary);
227+
}
228+
229+
.sortable:not(.active) .sort-indicator {
230+
opacity: 0;
231+
}
232+
233+
.sortable:hover:not(.active) .sort-indicator {
234+
opacity: 0.3;
235+
}
236+
182237
tbody tr:hover {
183238
background-color: var(--bg-primary);
184239
}

src/lib/merbench.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
FailureAnalysisData,
66
ParetoData,
77
ModelStats,
8+
LeaderboardEntry,
89
} from './merbench-types';
910

1011
// Calculate cost per run (simplified pricing model) - DEPRECATED
@@ -269,6 +270,60 @@ const calculateParetoFrontier = (data: Array<{ cost: number; Success_Rate: numbe
269270
return paretoPoints;
270271
};
271272

273+
// Sorting utilities
274+
let currentSortKey = 'Success_Rate';
275+
let currentSortDirection: 'asc' | 'desc' = 'desc';
276+
277+
export const sortLeaderboard = (
278+
data: LeaderboardEntry[],
279+
sortKey: string,
280+
direction: 'asc' | 'desc'
281+
): LeaderboardEntry[] => {
282+
const sorted = [...data].sort((a, b) => {
283+
let aVal: any;
284+
let bVal: any;
285+
286+
// Handle special cases for cost calculation
287+
if (sortKey === 'Avg_Cost') {
288+
aVal = a.Avg_Cost || calculateCost(a.Avg_Tokens);
289+
bVal = b.Avg_Cost || calculateCost(b.Avg_Tokens);
290+
} else {
291+
aVal = a[sortKey as keyof LeaderboardEntry];
292+
bVal = b[sortKey as keyof LeaderboardEntry];
293+
}
294+
295+
// Handle null/undefined values
296+
if (aVal == null && bVal == null) return 0;
297+
if (aVal == null) return direction === 'asc' ? -1 : 1;
298+
if (bVal == null) return direction === 'asc' ? 1 : -1;
299+
300+
// Numeric comparison
301+
if (typeof aVal === 'number' && typeof bVal === 'number') {
302+
return direction === 'asc' ? aVal - bVal : bVal - aVal;
303+
}
304+
305+
// String comparison
306+
const aStr = String(aVal).toLowerCase();
307+
const bStr = String(bVal).toLowerCase();
308+
309+
if (aStr < bStr) return direction === 'asc' ? -1 : 1;
310+
if (aStr > bStr) return direction === 'asc' ? 1 : -1;
311+
return 0;
312+
});
313+
314+
return sorted;
315+
};
316+
317+
export const setSortState = (sortKey: string, direction: 'asc' | 'desc'): void => {
318+
currentSortKey = sortKey;
319+
currentSortDirection = direction;
320+
};
321+
322+
export const getSortState = () => ({
323+
key: currentSortKey,
324+
direction: currentSortDirection,
325+
});
326+
272327
// DOM manipulation utilities
273328
export const updateSummaryStats = (filteredData: FilteredData): void => {
274329
const totalRuns = filteredData.rawData.length;

src/scripts/merbench-filters.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
import {
2-
getFilteredData,
3-
updateSummaryStats,
4-
updateLeaderboard,
5-
showEmptyState,
6-
} from '../lib/merbench';
1+
import { getFilteredData, updateSummaryStats, showEmptyState } from '../lib/merbench';
2+
import { updateLeaderboardData } from './merbench-sorting';
73
import type { RawData, TestGroupData, MerbenchData } from '../lib/merbench-types';
84
import { MerbenchCharts } from './merbench-charts';
95

@@ -317,7 +313,8 @@ export class MerbenchFilters {
317313

318314
private updateUI(filteredData: any): void {
319315
updateSummaryStats(filteredData);
320-
updateLeaderboard(filteredData);
316+
// Use sorting-aware leaderboard update instead of basic update
317+
updateLeaderboardData(filteredData.leaderboard);
321318
}
322319

323320
private showNoDataMessage(): void {

src/scripts/merbench-init-csp.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MerbenchCharts } from './merbench-charts';
22
import { MerbenchFilters } from './merbench-filters';
3+
import { initializeLeaderboardSorting } from './merbench-sorting';
34
import type { MerbenchData, RawData } from '../lib/merbench-types';
45

56
declare global {
@@ -82,6 +83,9 @@ async function initializeMerbench() {
8283
const filters = new MerbenchFilters(data, charts);
8384
filters.initialize();
8485

86+
// Initialize leaderboard sorting
87+
initializeLeaderboardSorting(originalData.leaderboard);
88+
8589
// Initialize charts with all data
8690
try {
8791
await charts.waitForPlotly();

src/scripts/merbench-sorting.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { sortLeaderboard, setSortState, getSortState, calculateCost } from '../lib/merbench';
2+
import type { LeaderboardEntry } from '../lib/merbench-types';
3+
4+
// Global leaderboard data storage
5+
let currentLeaderboardData: LeaderboardEntry[] = [];
6+
7+
// Initialize sorting functionality
8+
export const initializeLeaderboardSorting = (leaderboardData: LeaderboardEntry[]): void => {
9+
currentLeaderboardData = [...leaderboardData];
10+
setupSortHandlers();
11+
};
12+
13+
// Update leaderboard data (called when filters change)
14+
export const updateLeaderboardData = (newData: LeaderboardEntry[]): void => {
15+
currentLeaderboardData = [...newData];
16+
17+
// Apply current sort to the new data
18+
const sortState = getSortState();
19+
const sortedData = sortLeaderboard(currentLeaderboardData, sortState.key, sortState.direction);
20+
renderLeaderboard(sortedData);
21+
};
22+
23+
// Setup click handlers for sortable headers
24+
const setupSortHandlers = (): void => {
25+
const sortableHeaders = document.querySelectorAll('.sortable');
26+
27+
sortableHeaders.forEach((header) => {
28+
header.addEventListener('click', (event) => {
29+
const target = event.currentTarget as HTMLElement;
30+
const sortKey = target.dataset.sortKey;
31+
const sortType = target.dataset.sortType;
32+
33+
if (!sortKey) return;
34+
35+
// Determine new sort direction
36+
const currentDirection = target.dataset.sortDirection;
37+
let newDirection: 'asc' | 'desc';
38+
39+
if (target.classList.contains('active')) {
40+
// Toggle direction if clicking the same column
41+
newDirection = currentDirection === 'desc' ? 'asc' : 'desc';
42+
} else {
43+
// Default direction for new column
44+
newDirection = sortType === 'string' ? 'asc' : 'desc';
45+
}
46+
47+
// Update sort state
48+
setSortState(sortKey, newDirection);
49+
50+
// Update UI
51+
updateSortIndicators(sortKey, newDirection);
52+
53+
// Sort and render data
54+
const sortedData = sortLeaderboard(currentLeaderboardData, sortKey, newDirection);
55+
renderLeaderboard(sortedData);
56+
});
57+
});
58+
};
59+
60+
// Update visual sort indicators
61+
const updateSortIndicators = (activeSortKey: string, direction: 'asc' | 'desc'): void => {
62+
const sortableHeaders = document.querySelectorAll('.sortable');
63+
64+
sortableHeaders.forEach((header) => {
65+
const element = header as HTMLElement;
66+
const sortKey = element.dataset.sortKey;
67+
const indicator = element.querySelector('.sort-indicator');
68+
69+
if (!indicator) return;
70+
71+
if (sortKey === activeSortKey) {
72+
// Active column
73+
element.classList.add('active');
74+
element.dataset.sortDirection = direction;
75+
indicator.textContent = direction === 'desc' ? '↓' : '↑';
76+
} else {
77+
// Inactive columns
78+
element.classList.remove('active');
79+
element.removeAttribute('data-sort-direction');
80+
indicator.textContent = '';
81+
}
82+
});
83+
};
84+
85+
// Render the leaderboard table
86+
const renderLeaderboard = (data: LeaderboardEntry[]): void => {
87+
const tbody = document.querySelector('#leaderboard-table tbody');
88+
if (!tbody) return;
89+
90+
// Calculate cost range for progress bar normalization
91+
const costs = data.map((entry) => entry.Avg_Cost || calculateCost(entry.Avg_Tokens));
92+
const minCost = Math.min(...costs);
93+
const maxCost = Math.max(...costs);
94+
const costRange = maxCost - minCost;
95+
96+
tbody.innerHTML = data
97+
.map((entry, index) => {
98+
const currentCost = entry.Avg_Cost || calculateCost(entry.Avg_Tokens);
99+
const costWidth = costRange > 0 ? (currentCost / maxCost) * 100 : 0;
100+
101+
return `
102+
<tr>
103+
<td class="rank">${index + 1}</td>
104+
<td class="model-name">${entry.Model}</td>
105+
<td class="success-rate">
106+
<div class="progress-bar">
107+
<div class="progress-fill" style="width: ${entry.Success_Rate}%; background-color: ${
108+
entry.Success_Rate >= 30 ? '#27ae60' : entry.Success_Rate >= 15 ? '#f39c12' : '#e74c3c'
109+
}"></div>
110+
<span class="progress-text">${entry.Success_Rate.toFixed(1)}%</span>
111+
</div>
112+
</td>
113+
<td class="cost">
114+
<div class="progress-bar">
115+
<div class="progress-fill progress-fill--cost" style="width: ${costWidth}%"></div>
116+
<span class="progress-text">$${currentCost.toFixed(4)}</span>
117+
</div>
118+
</td>
119+
<td class="duration">${entry.Avg_Duration.toFixed(2)}s</td>
120+
<td class="tokens">${entry.Avg_Tokens.toLocaleString()}</td>
121+
<td class="runs">${entry.Runs}</td>
122+
<td class="provider">${entry.Provider}</td>
123+
</tr>
124+
`;
125+
})
126+
.join('');
127+
};

0 commit comments

Comments
 (0)