diff --git a/src/components/DatasetCompositionChart.css b/src/components/DatasetCompositionChart.css index 240742f..5172eb8 100644 --- a/src/components/DatasetCompositionChart.css +++ b/src/components/DatasetCompositionChart.css @@ -92,6 +92,7 @@ .pie-chart-container { margin: 1rem 0; + min-height: 420px; } } @@ -103,4 +104,8 @@ .chart-title { font-size: 1.1rem; } + + .pie-chart-container { + min-height: 300px; + } } diff --git a/src/components/DatasetCompositionChart.tsx b/src/components/DatasetCompositionChart.tsx index b5062c1..8ae2485 100644 --- a/src/components/DatasetCompositionChart.tsx +++ b/src/components/DatasetCompositionChart.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, LabelList } from 'recharts'; import './DatasetCompositionChart.css'; @@ -105,13 +105,49 @@ const DatasetCompositionChart: React.FC = () => { const [hoveredCategory, setHoveredCategory] = useState(null); const [hoveredSubcategory, setHoveredSubcategory] = useState(null); - // Define chart dimensions as constants for easy adjustment - const CHART_DIMENSIONS = { - innerRadius: 100, - outerRadius: 250, - labelRadius: 330, // Distance from center to labels - chartSize: 800, // Container height - }; + const [viewportWidth, setViewportWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 1200 + ); + + useEffect(() => { + if (typeof window === 'undefined') return; + const handleResize = () => setViewportWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const CHART_DIMENSIONS = useMemo(() => { + if (viewportWidth <= 480) { + return { + innerRadius: 50, + outerRadius: 125, + labelRadius: 185, + chartSize: 320, + }; + } + if (viewportWidth <= 768) { + return { + innerRadius: 70, + outerRadius: 170, + labelRadius: 240, + chartSize: 460, + }; + } + if (viewportWidth <= 1024) { + return { + innerRadius: 90, + outerRadius: 210, + labelRadius: 290, + chartSize: 620, + }; + } + return { + innerRadius: 100, + outerRadius: 250, + labelRadius: 330, + chartSize: 800, + }; + }, [viewportWidth]); // Prepare data for the main pie chart (outer ring) const prepareMainChartData = (): ChartData[] => { @@ -323,7 +359,7 @@ const DatasetCompositionChart: React.FC = () => { pointerEvents: 'none', zIndex: 0, }} - viewBox="0 0 800 800" + viewBox={`0 0 ${CHART_DIMENSIONS.chartSize} ${CHART_DIMENSIONS.chartSize}`} > {mainChartData.map((entry, index) => { const totalValue = mainChartData.reduce((sum, item) => sum + item.value, 0); @@ -347,8 +383,8 @@ const DatasetCompositionChart: React.FC = () => { const radius = CHART_DIMENSIONS.outerRadius + baseOffset + sideBoost * angleFactor; // Use exact center coordinates matching Recharts - const centerX = 400; // Exact center of 800x800 viewBox - const centerY = 400; // Exact center of 800x800 viewBox + const centerX = CHART_DIMENSIONS.chartSize / 2; + const centerY = CHART_DIMENSIONS.chartSize / 2; const x = centerX - radius * Math.sin(midAngle * RADIAN); const y = centerY - radius * Math.cos(midAngle * RADIAN); diff --git a/src/components/DeferralCurve.css b/src/components/DeferralCurve.css index c9e2125..d04d843 100644 --- a/src/components/DeferralCurve.css +++ b/src/components/DeferralCurve.css @@ -19,6 +19,8 @@ .deferral-curve-wrapper svg { overflow: visible; flex-shrink: 0; + height: auto; + max-width: 500px; } .deferral-legend-side { @@ -99,3 +101,15 @@ grid-template-columns: repeat(2, 1fr); } } + +@media (max-width: 640px) { + .deferral-legend-side { + flex-direction: column; + align-items: stretch; + gap: 1.5rem; + } + + .legend-items { + grid-template-columns: 1fr; + } +} diff --git a/src/components/DeferralCurve.tsx b/src/components/DeferralCurve.tsx index f2e2677..75ad8ed 100644 --- a/src/components/DeferralCurve.tsx +++ b/src/components/DeferralCurve.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import './DeferralCurve.css'; interface DeferralCurveProps { @@ -11,6 +11,44 @@ interface DeferralCurveProps { } const DeferralCurve: React.FC = ({ openSourcePoints, closedSourcePoints }) => { + const [viewportWidth, setViewportWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 1200 + ); + + useEffect(() => { + if (typeof window === 'undefined') return; + const handleResize = () => setViewportWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const chartLayout = useMemo(() => { + if (viewportWidth <= 480) { + return { + width: 320, + height: 260, + margin: { top: 15, right: 10, bottom: 50, left: 60 }, + pointSize: 5, + fontScale: 0.78, + }; + } + if (viewportWidth <= 768) { + return { + width: 400, + height: 320, + margin: { top: 20, right: 15, bottom: 55, left: 70 }, + pointSize: 6, + fontScale: 0.9, + }; + } + return { + width: 500, + height: 400, + margin: { top: 20, right: 20, bottom: 60, left: 80 }, + pointSize: 6.5, + fontScale: 1, + }; + }, [viewportWidth]); // Extract all accuracy and cost values const allPoints = [...Object.values(openSourcePoints), ...Object.values(closedSourcePoints)]; const accuracyValues = allPoints.map(p => p.accuracy); @@ -32,9 +70,9 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS const costMax = maxCost * 1.2; // Extend a bit beyond maximum // Chart dimensions - const chartWidth = 500; - const chartHeight = 400; - const margin = { top: 20, right: 20, bottom: 60, left: 80 }; + const chartWidth = chartLayout.width; + const chartHeight = chartLayout.height; + const margin = chartLayout.margin; const plotWidth = chartWidth - margin.left - margin.right; const plotHeight = chartHeight - margin.top - margin.bottom; @@ -67,8 +105,14 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS ]; // Function to render different shapes - const renderShape = (x: number, y: number, shape: string, color: string, key: string) => { - const size = 6; + const renderShape = ( + x: number, + y: number, + shape: string, + color: string, + key: string, + size: number = chartLayout.pointSize + ) => { switch (shape) { case 'square': return ( @@ -163,6 +207,7 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS }; const costTicks = generatePowersOf10(costMin, costMax); + const scaledFont = (size: number) => size * chartLayout.fontScale; return (
@@ -234,7 +279,7 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS = ({ openSourcePoints, closedS @@ -303,7 +348,7 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS @@ -319,7 +364,7 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS = ({ openSourcePoints, closedS = ({ openSourcePoints, closedS return (
- {renderShape(8, 8, 'circle', color, `legend-${name}`)} + {renderShape(8, 8, 'circle', color, `legend-${name}`, 5)} {name}
@@ -367,7 +412,7 @@ const DeferralCurve: React.FC = ({ openSourcePoints, closedS return (
- {renderShape(8, 8, 'triangle', color, `legend-${name}`)} + {renderShape(8, 8, 'triangle', color, `legend-${name}`, 5)} {name}
diff --git a/src/components/SpiderChart.css b/src/components/SpiderChart.css index 6ad9fef..15aafcc 100644 --- a/src/components/SpiderChart.css +++ b/src/components/SpiderChart.css @@ -12,7 +12,7 @@ border-radius: 0; padding: 0; box-shadow: none; - max-width: 600px; + max-width: 100%; margin: 0 auto; overflow: visible; width: 100%; @@ -20,6 +20,9 @@ .spider-chart svg { overflow: visible; + width: 100%; + height: auto; + max-width: 450px; } .metric-label { diff --git a/src/components/SpiderChart.tsx b/src/components/SpiderChart.tsx index 7bc3f53..65a2e05 100644 --- a/src/components/SpiderChart.tsx +++ b/src/components/SpiderChart.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { Router } from '../types'; import './SpiderChart.css'; @@ -8,6 +8,27 @@ interface SpiderChartProps { } const SpiderChart: React.FC = ({ routers, maxRouters = 5 }) => { + const [viewportWidth, setViewportWidth] = useState( + typeof window !== 'undefined' ? window.innerWidth : 1200 + ); + + useEffect(() => { + if (typeof window === 'undefined') return; + const handleResize = () => setViewportWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + const chartLayout = useMemo(() => { + if (viewportWidth <= 480) { + return { size: 300, radius: 120, labelOffset: 28, fontScale: 0.8 }; + } + if (viewportWidth <= 768) { + return { size: 360, radius: 150, labelOffset: 35, fontScale: 0.9 }; + } + return { size: 450, radius: 180, labelOffset: 40, fontScale: 1 }; + }, [viewportWidth]); + // Get top N routers by overall rank const topRouters = routers .sort((a, b) => a.metrics.overallRank - b.metrics.overallRank) @@ -71,14 +92,18 @@ const SpiderChart: React.FC = ({ routers, maxRouters = 5 }) => const axisMax = 100; const axisRange = axisMax - axisMin; - const chartRadius = 180; - const centerX = 225; - const centerY = 225; + const chartRadius = chartLayout.radius; + const centerX = chartLayout.size / 2; + const centerY = chartLayout.size / 2; return (
- + {/* Grid circles drawn at fixed 0-100 scale */} {gridTicks.map((value, index) => { const scale = (value - axisMin) / axisRange; @@ -101,7 +126,7 @@ const SpiderChart: React.FC = ({ routers, maxRouters = 5 }) => dominantBaseline="middle" className="grid-label" fill="#9ca3af" - fontSize="22" + fontSize={22 * chartLayout.fontScale} > {value.toString()} @@ -131,8 +156,8 @@ const SpiderChart: React.FC = ({ routers, maxRouters = 5 }) => {/* Metric labels */} {metrics.map((metric, index) => { const angle = (index * 2 * Math.PI) / metrics.length - Math.PI / 2; - const labelX = centerX + Math.cos(angle) * (chartRadius + 40); - const labelY = centerY + Math.sin(angle) * (chartRadius + 40); + const labelX = centerX + Math.cos(angle) * (chartRadius + chartLayout.labelOffset); + const labelY = centerY + Math.sin(angle) * (chartRadius + chartLayout.labelOffset); return ( @@ -143,7 +168,7 @@ const SpiderChart: React.FC = ({ routers, maxRouters = 5 }) => dominantBaseline="middle" className="metric-label" fill="#1f2937" - fontSize="16" + fontSize={16 * chartLayout.fontScale} fontWeight="600" > {metric.label} diff --git a/src/data/mockData.ts b/src/data/mockData.ts index 0a422e9..c6c056f 100644 --- a/src/data/mockData.ts +++ b/src/data/mockData.ts @@ -391,8 +391,8 @@ const routersWithRanks = rawRouterData.map(router => { optimalAccScore: roundNullableToOneDecimal(router['Optimal Acc. Score']), robustnessScore: roundNullableToOneDecimal(router['Robustness Score']), latencyScore: roundNullableToOneDecimal(router['Latency Score']), - accuracy: roundToOneDecimal(router['Accuracy']), - costPer1k: roundToOneDecimal(router['Cost per 1k']), + accuracy: router['Accuracy'], + costPer1k: router['Cost per 1k'], overallRank: 0, // Will be calculated below }; diff --git a/src/pages/HomePage.css b/src/pages/HomePage.css index 6f0d595..a0dd9c7 100644 --- a/src/pages/HomePage.css +++ b/src/pages/HomePage.css @@ -621,13 +621,45 @@ .hero-content { grid-template-columns: 1fr; gap: 2rem; - text-align: center; + text-align: left; } .hero-title { - font-size: 2rem; + font-size: 2.8rem; + text-align: left; + } + + .hero-right { + width: 100%; + display: flex; + justify-content: center; + } + + .hero-right-content { + float: none; + width: 100%; + justify-content: center; + align-items: center; + } + + .hero-cards-container { + align-items: center; + justify-content: center; + text-align: center; + margin-left: 0; + margin-right: 0; + width: 100%; + } + + .hero-card, + .cta-card { + max-width: 320px; + width: 100%; + text-align: center; } + + .hero-actions { justify-content: center; } @@ -662,17 +694,8 @@ max-width: 100%; } - .cta-card { - max-width: 300px; - } - .cta-heading { - font-size: 1.1rem; - } - .cta-description { - font-size: 1.1rem; - } } /* Responsive styles for tabs */ diff --git a/src/pages/LeaderboardPage.css b/src/pages/LeaderboardPage.css index 04a1c74..5d90387 100644 --- a/src/pages/LeaderboardPage.css +++ b/src/pages/LeaderboardPage.css @@ -41,6 +41,50 @@ text-shadow: none; } +@media (max-width: 1024px) { + .leaderboard-page .page-title { + font-size: 2.2rem; + } + + .title-icon { + width: 30px; + height: 30px; + } +} + +@media (max-width: 768px) { + .leaderboard-page .page-title { + font-size: 1.9rem; + gap: 0.75rem; + } + + .leaderboard-page .page-subtitle { + font-size: 1rem; + padding: 0 1rem; + } + + .title-icon { + width: 26px; + height: 26px; + } +} + +@media (max-width: 480px) { + .leaderboard-page .page-title { + font-size: 1.6rem; + flex-wrap: wrap; + } + + .leaderboard-page .page-subtitle { + font-size: 0.95rem; + } + + .title-icon { + width: 24px; + height: 24px; + } +} + .metric-filters { display: flex; gap: 1rem; @@ -124,6 +168,38 @@ flex-wrap: wrap; } +.beta-control { + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 200px; +} + +.beta-label { + display: flex; + justify-content: space-between; + font-size: 0.9rem; + font-weight: 600; + color: #1f2937; +} + +.beta-value { + font-variant-numeric: tabular-nums; + color: #2563eb; +} + +.beta-slider { + width: 100%; + accent-color: #2563eb; +} + +.beta-hints { + display: flex; + justify-content: space-between; + font-size: 0.75rem; + color: #6b7280; +} + .filter-group { display: flex; flex-direction: column; @@ -156,12 +232,32 @@ background: white; border-radius: 12px; border: 1px solid #e5e7eb; - overflow: hidden; /* no horizontal scroll */ + overflow: hidden; margin-bottom: 2rem; width: 100%; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } +.leaderboard-scroll { + width: 100%; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; +} + +.leaderboard-scroll::-webkit-scrollbar { + height: 8px; +} + +.leaderboard-scroll::-webkit-scrollbar-thumb { + background: #cbd5f5; + border-radius: 999px; +} + +.leaderboard-scroll::-webkit-scrollbar-track { + background: transparent; +} + :root { --lb-grid: @@ -197,7 +293,6 @@ .leaderboard-header > div, .leaderboard-row > div { - min-width: 0 !important; padding: 0.75rem 0.75rem; overflow: hidden; text-overflow: ellipsis; @@ -577,6 +672,18 @@ z-index: 0; } +@media (max-width: 1024px) { + .metrics-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .metrics-grid { + grid-template-columns: minmax(0, 1fr); + } +} + .metrics-explanation { margin-bottom: 15rem; /* give plenty of space at bottom */ position: relative; @@ -754,30 +861,16 @@ -@media (max-width: 768px) { +@media (max-width: 1200px) { .leaderboard-header, .leaderboard-row { - grid-template-columns: 1fr; - gap: 1rem; - text-align: center; - } - - .leaderboard-header { - display: none; - } - - .leaderboard-row { - display: flex; - flex-direction: column; - padding: 1rem; - border: 1px solid #e5e7eb; - border-radius: 8px; - margin-bottom: 1rem; + min-width: 1200px; } .controls { flex-direction: column; align-items: stretch; + gap: 1rem; } .search-box { @@ -788,6 +881,10 @@ justify-content: center; } + .beta-control { + min-width: auto; + } + .viz-tabs { flex-direction: column; align-items: center; @@ -820,7 +917,6 @@ overflow: hidden; /* text-overflow: ellipsis; */ white-space: nowrap; - min-width: 0 !important; } diff --git a/src/pages/LeaderboardPage.tsx b/src/pages/LeaderboardPage.tsx index 8637177..03be339 100644 --- a/src/pages/LeaderboardPage.tsx +++ b/src/pages/LeaderboardPage.tsx @@ -9,6 +9,28 @@ import 'katex/dist/katex.min.css'; import { InlineMath, BlockMath } from 'react-katex'; import huggingFaceLogo from '../assets/images/hf-logo.svg'; +type RouterWithDynamicArena = Router & { dynamicArena: number }; + +const COST_MIN = 0.0044; +const COST_MAX = 200; + +const computeNormalizedCost = (costPer1k: number): number => { + // const safeCost = Math.max(costPer1k, COST_MIN); + const numerator = Math.log2(COST_MAX) - Math.log2(costPer1k); + const denominator = Math.log2(COST_MAX) - Math.log2(COST_MIN); + if (denominator === 0) return 0; + const normalized = numerator / denominator; + return Math.min(Math.max(normalized, 0), 1); +}; + +const computeArenaScore = (router: Router, beta: number): number => { + const accuracy = router.metrics.accuracy / 100; + const normalizedCost = computeNormalizedCost(router.metrics.costPer1k); + const denominator = beta * accuracy + normalizedCost; + if (denominator === 0) return 0; + return (((1 + beta) * accuracy * normalizedCost) / denominator) * 100; +}; + const LeaderboardPage: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); const [filterType, setFilterType] = useState<'all' | 'open-source' | 'closed-source'>('all'); @@ -16,6 +38,7 @@ const LeaderboardPage: React.FC = () => { 'arena' | 'accuracy' | 'cost' | 'optimalSelection' | 'optimalCost' | 'optimalAcc' | 'latency' | 'robustness' >('arena'); const [activeTab, setActiveTab] = useState<'spider' | 'deferral'>('spider'); + const [beta, setBeta] = useState(0.1); // Deferral curve data const openSourcePoints = { @@ -47,7 +70,7 @@ const LeaderboardPage: React.FC = () => { return scores.reduce((sum, score) => sum + score, 0) / scores.length; }; - const filteredAndSortedRouters = useMemo(() => { + const filteredAndSortedRouters = useMemo(() => { const metricKeyMap = { arena: 'arenaScore', optimalSelection: 'optimalSelectionScore', @@ -59,7 +82,12 @@ const LeaderboardPage: React.FC = () => { cost: 'costPer1k', } as const; - let filtered = routers.filter(router => { + const withDynamicArena: RouterWithDynamicArena[] = routers.map(router => ({ + ...router, + dynamicArena: computeArenaScore(router, beta), + })); + + let filtered = withDynamicArena.filter(router => { const matchesSearch = router.name.toLowerCase().includes(searchTerm.toLowerCase()) || router.description.toLowerCase().includes(searchTerm.toLowerCase()); @@ -69,8 +97,8 @@ const LeaderboardPage: React.FC = () => { const key = metricKeyMap[activeMetric]; return filtered.sort((a, b) => { - const scoreA = a.metrics[key]; - const scoreB = b.metrics[key]; + const scoreA = activeMetric === 'arena' ? a.dynamicArena : a.metrics[key]; + const scoreB = activeMetric === 'arena' ? b.dynamicArena : b.metrics[key]; // Handle null values - put them at the end if (scoreA === null && scoreB === null) return 0; if (scoreA === null) return 1; @@ -82,7 +110,7 @@ const LeaderboardPage: React.FC = () => { // For all other metrics, higher is better, so sort descending return (scoreB as number) - (scoreA as number); }); - }, [searchTerm, filterType, activeMetric]); + }, [searchTerm, filterType, activeMetric, beta]); // const getRankBadge = (rank: number) => { // if (rank === 1) return 'rank-1'; @@ -209,27 +237,49 @@ const LeaderboardPage: React.FC = () => {
+ +
+
+ Arena β + {beta.toFixed(2)} +
+ setBeta(parseFloat(event.target.value))} + className="beta-slider" + /> +
+ Accuracy-first + Cost-first +
+
{/* Leaderboard Table */}
-
-
Rank
-
Router
-
Affiliation
-
Type
-
Arena
-
Accuracy
-
Cost/1K
-
Opt. Select
-
Opt. Cost
-
Opt. Acc
-
Latency
-
Robust
-
+
+
+
Rank
+
Router
+
Affiliation
+
Type
+
Arena
+
Accuracy
+
Cost/1K
+
Opt. Select
+
Opt. Cost
+
Opt. Acc
+
Latency
+
Robust
+
-
- {filteredAndSortedRouters.map((router, index) => { +
+ {filteredAndSortedRouters.map((router, index) => { const primaryLink = router.websiteUrl || router.paperUrl || router.githubUrl; return (
@@ -288,7 +338,7 @@ const LeaderboardPage: React.FC = () => {
- {router.metrics.arenaScore.toFixed(1)} + {router.dynamicArena.toFixed(1)}
@@ -354,9 +404,10 @@ const LeaderboardPage: React.FC = () => {
-
- ); - })} +
+ ); + })} +