|
|
<script setup> |
|
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue' |
|
|
import { useLeaderboardData } from '@/composables/useLeaderboardData.js' |
|
|
import * as echarts from 'echarts' |
|
|
|
|
|
const chartRef = ref(null) |
|
|
let chart = null |
|
|
|
|
|
|
|
|
const isDarkChart = ref(document.documentElement.classList.contains('dark')) |
|
|
|
|
|
let themeObserver = null |
|
|
|
|
|
const { leaderboard, selectedDataNameChart, modelTypeGroups, strtSymbolSeries } = useLeaderboardData() |
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({ |
|
|
autoShowSeries: { type: Array, default: () => [] } |
|
|
}) |
|
|
|
|
|
|
|
|
function buildSeriesFromLeaderboard(rows = []) { |
|
|
const groups = {} |
|
|
rows.forEach(r => { |
|
|
|
|
|
|
|
|
const baseType = r.model_type || '未知' |
|
|
const isMoE = !!r._isMoE |
|
|
const seriesName = isMoE ? `${baseType} (MoE)` : baseType |
|
|
if (!groups[seriesName]) groups[seriesName] = [] |
|
|
|
|
|
const xRaw = parseFloat(r.BF16_TFLOPs) || 0 |
|
|
const x = xRaw > 0 ? xRaw * 1e12 / 1024 : 0 |
|
|
|
|
|
const y = parseFloat(r.ic) || 0 |
|
|
const rank = Number(r.rank) || 0 |
|
|
groups[seriesName].push({ x, y, rank }) |
|
|
}) |
|
|
|
|
|
return Object.keys(groups).map(name => { |
|
|
|
|
|
const data = groups[name] |
|
|
.filter(p => Number.isFinite(p.x) && p.x > 0 && Number.isFinite(p.y)) |
|
|
.sort((a, b) => a.x - b.x) |
|
|
.map(d => [d.x, d.y]) |
|
|
|
|
|
const count = groups[name].length || 1 |
|
|
const symbolSize = Math.min(18, 6 + Math.sqrt(count) * 1.6) |
|
|
return { name, rawCount: count, type: 'line', showSymbol: true, symbolSize, data } |
|
|
}) |
|
|
} |
|
|
|
|
|
function getChartOption(series, isDark = false) { |
|
|
|
|
|
const titleColor = isDark ? '#cfd8dc' : '#333333' |
|
|
const legendColor = isDark ? '#cfd8dc' : '#333333' |
|
|
const axisLineColor = isDark ? '#78909c' : '#666666' |
|
|
const axisLabelColor = isDark ? '#b0bec5' : '#666666' |
|
|
const splitLineColor = isDark ? '#37474f' : '#cccccc' |
|
|
|
|
|
const allX = [] |
|
|
const allY = [] |
|
|
series.forEach(s => { |
|
|
if (Array.isArray(s.data)) { |
|
|
s.data.forEach(d => { |
|
|
const x = Number(d[0]) |
|
|
const y = Number(d[1]) |
|
|
if (Number.isFinite(x)) allX.push(x) |
|
|
if (Number.isFinite(y)) allY.push(y) |
|
|
}) |
|
|
} |
|
|
}) |
|
|
let xMin = allX.length ? Math.min(...allX) : undefined |
|
|
let xMax = allX.length ? Math.max(...allX) : undefined |
|
|
let yMin = allY.length ? Math.min(...allY) : undefined |
|
|
let yMax = allY.length ? Math.max(...allY) : undefined |
|
|
|
|
|
|
|
|
if (xMin !== undefined && xMax !== undefined) { |
|
|
|
|
|
if (xMin <= 0) xMin = Math.min(...allX.filter(v => v > 0)) || xMax |
|
|
if (xMin === xMax) { |
|
|
xMin = xMin / 10 |
|
|
xMax = xMax * 10 |
|
|
} else { |
|
|
xMin = xMin * 0.8 |
|
|
xMax = xMax * 1.15 |
|
|
} |
|
|
} |
|
|
|
|
|
if (yMin !== undefined && yMax !== undefined) { |
|
|
if (yMin === yMax) { |
|
|
|
|
|
|
|
|
const v = (yMin !== undefined ? yMin : 1) |
|
|
if (v === 0) { |
|
|
|
|
|
const pad = 0.05 |
|
|
yMin = -pad |
|
|
yMax = pad |
|
|
} else { |
|
|
yMin = v - Math.abs(v) * 0.05 |
|
|
yMax = v + Math.abs(v) * 0.05 |
|
|
} |
|
|
} else { |
|
|
const pad = (yMax - yMin) * 0.08 |
|
|
yMin = yMin - pad |
|
|
yMax = yMax + pad |
|
|
} |
|
|
|
|
|
if (yMin < 0 && Math.min(...allY) >= 0) yMin = 0 |
|
|
} |
|
|
|
|
|
|
|
|
function toSuperscript(n) { |
|
|
const map = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹', '-': '⁻' } |
|
|
const s = String(n) |
|
|
return s.split('').map(ch => map[ch] || ch).join('') |
|
|
} |
|
|
|
|
|
|
|
|
const colors = ['#1F77B4', '#ff7f0e', '#2CA02C', '#D62728', '#9467BD', '#8C564B', '#E377C2', '#7F7F7F', '#BCBD22', '#17BECF'] |
|
|
const lineStyles = ['solid', 'solid', 'solid', 'solid', 'dash', 'solid', 'solid'] |
|
|
|
|
|
|
|
|
const rawSeries = series.map(s => { |
|
|
const baseSize = Number(s.symbolSize) || 8 |
|
|
return Object.assign({}, s, { symbolSize: baseSize, hoverAnimation: true }) |
|
|
}) |
|
|
|
|
|
|
|
|
const legendSelected = {} |
|
|
if (props.autoShowSeries.includes("*")) { |
|
|
rawSeries.forEach(s => { |
|
|
const name = String(s.name) |
|
|
legendSelected[name] = true |
|
|
}) |
|
|
} |
|
|
else { |
|
|
rawSeries.forEach(s => { |
|
|
const name = String(s.name) |
|
|
legendSelected[name] = Array.isArray(props.autoShowSeries) && props.autoShowSeries.includes(name) |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const preferredOrder = (modelTypeGroups && Array.isArray(modelTypeGroups.value)) ? modelTypeGroups.value : [] |
|
|
const orderIndex = {} |
|
|
preferredOrder.forEach((n, i) => { orderIndex[String(n)] = i }) |
|
|
|
|
|
const inOrder = [] |
|
|
const rest = [] |
|
|
rawSeries.forEach(s => { |
|
|
const name = String(s.name) |
|
|
if (Object.prototype.hasOwnProperty.call(orderIndex, name)) { |
|
|
inOrder.push(s) |
|
|
} else { |
|
|
rest.push(s) |
|
|
} |
|
|
}) |
|
|
|
|
|
inOrder.sort((a, b) => orderIndex[String(a.name)] - orderIndex[String(b.name)]) |
|
|
|
|
|
rest.sort((a, b) => String(a.name).localeCompare(String(b.name))) |
|
|
|
|
|
|
|
|
|
|
|
const finalSeries = [...inOrder, ...rest] |
|
|
|
|
|
|
|
|
const finalSeriesStyled = finalSeries.map((s, idx) => { |
|
|
const color = colors[idx % colors.length] |
|
|
|
|
|
const isMoESeries = String(s.name).includes('(MoE)') |
|
|
const symbolShape = strtSymbolSeries.includes(String(s.name)) ? |
|
|
'path://M341.5 45.1C337.4 37.1 329.1 32 320.1 32C311.1 32 302.8 37.1 298.7 45.1L225.1 189.3L65.2 214.7C56.3 216.1 48.9 222.4 46.1 231C43.3 239.6 45.6 249 51.9 255.4L166.3 369.9L141.1 529.8C139.7 538.7 143.4 547.7 150.7 553C158 558.3 167.6 559.1 175.7 555L320.1 481.6L464.4 555C472.4 559.1 482.1 558.3 489.4 553C496.7 547.7 500.4 538.8 499 529.8L473.7 369.9L588.1 255.4C594.5 249 596.7 239.6 593.9 231C591.1 222.4 583.8 216.1 574.8 214.7L415 189.3L341.5 45.1z' |
|
|
: 'circle' |
|
|
const lineType = symbolShape !== 'circle' ? 'dashed' : (lineStyles[idx % lineStyles.length] || 'solid') |
|
|
|
|
|
const baseSize = Number(s.symbolSize) || 8 |
|
|
const emphasisSize = Math.max(12, Math.round(baseSize * 1.6)) |
|
|
|
|
|
const adjustedBaseSize = symbolShape !== 'circle' ? baseSize * 1.5 : baseSize |
|
|
const adjustedEmphasisSize = symbolShape !== 'circle' ? emphasisSize * 1.5 : emphasisSize |
|
|
return Object.assign({}, s, { |
|
|
lineStyle: { width: 2, type: lineType, color }, |
|
|
itemStyle: { |
|
|
borderColor: '#f3f3f3', |
|
|
borderWidth: symbolShape !== 'circle' ? 1 : 0, |
|
|
borderCap: 'round', |
|
|
borderRadius: 1, |
|
|
shadowBlur: 0, |
|
|
shadowColor: '#45DB76' |
|
|
}, |
|
|
symbol: symbolShape, |
|
|
symbolSize: adjustedBaseSize, |
|
|
hoverAnimation: true, |
|
|
emphasis: { |
|
|
focus: 'series', |
|
|
symbolSize: adjustedEmphasisSize, |
|
|
itemStyle: { borderWidth: 2, borderColor: '#ffffff' } |
|
|
} |
|
|
}) |
|
|
}) |
|
|
const legendData = finalSeriesStyled.map(s => String(s.name)) |
|
|
|
|
|
return { |
|
|
color: colors, |
|
|
tooltip: { |
|
|
trigger: 'item', |
|
|
formatter: params => { |
|
|
if (!params) return '' |
|
|
const x = params.data && Array.isArray(params.data) ? params.data[0] : '' |
|
|
const y = params.data && Array.isArray(params.data) ? params.data[1] : '' |
|
|
|
|
|
let xStr = '' |
|
|
if (typeof x === 'number' && Number.isFinite(x) && x > 0) { |
|
|
const exp = Math.floor(Math.log10(x)) |
|
|
const mant = x / Math.pow(10, exp) |
|
|
const mantStr = mant >= 10 ? mant.toFixed(0) : mant.toFixed(2) |
|
|
xStr = `${mantStr}×10${toSuperscript(exp)}` |
|
|
} else { |
|
|
xStr = String(x) |
|
|
} |
|
|
const yStr = (typeof y === 'number' && Number.isFinite(y)) ? Number(y).toFixed(4) : String(y) |
|
|
return `${params.seriesName}<br/>FLOPs: ${xStr}<br/>IC: ${yStr}` |
|
|
} |
|
|
}, |
|
|
legend: Object.assign({ |
|
|
orient: 'vertical', |
|
|
right: 10, |
|
|
top: '10%', |
|
|
align: 'left', |
|
|
itemWidth: 14, |
|
|
itemHeight: 10, |
|
|
textStyle: { color: legendColor }, |
|
|
tooltip: { show: true } |
|
|
}, { selected: legendSelected, data: legendData }), |
|
|
grid: { left: '6%', right: '20%', bottom: '8%', containLabel: true }, |
|
|
xAxis: { |
|
|
type: 'log', |
|
|
name: 'FLOPs', |
|
|
nameLocation: 'middle', |
|
|
nameGap: 30, |
|
|
axisLine: { lineStyle: { color: axisLineColor } }, |
|
|
axisLabel: { |
|
|
color: axisLabelColor, |
|
|
formatter: value => { |
|
|
if (!Number.isFinite(value) || value <= 0) return String(value) |
|
|
const exp = Math.floor(Math.log10(value)) |
|
|
const mant = value / Math.pow(10, exp) |
|
|
if (Math.abs(mant - 1) < 1e-6) return `10${toSuperscript(exp)}` |
|
|
return `${mant.toFixed(2)}×10${toSuperscript(exp)}` |
|
|
} |
|
|
}, |
|
|
splitLine: { show: true, lineStyle: { type: 'dashed', color: splitLineColor } }, |
|
|
min: xMin, |
|
|
max: xMax |
|
|
}, |
|
|
yAxis: { |
|
|
type: 'value', |
|
|
name: 'Information capacity', |
|
|
axisLine: { lineStyle: { color: axisLineColor } }, |
|
|
axisLabel: { color: axisLabelColor, formatter: v => (Number.isFinite(v) ? Number(v).toFixed(2) : String(v)) }, |
|
|
splitLine: { show: true, lineStyle: { type: 'dashed', color: splitLineColor } }, |
|
|
min: yMin, |
|
|
max: yMax |
|
|
}, |
|
|
series: finalSeriesStyled |
|
|
} |
|
|
} |
|
|
|
|
|
let resizeHandler = null |
|
|
|
|
|
function renderChart() { |
|
|
if (!chart) return |
|
|
let rows = leaderboard.value || [] |
|
|
|
|
|
const sel = selectedDataNameChart && selectedDataNameChart.value ? String(selectedDataNameChart.value) : '' |
|
|
if (sel && sel !== 'all') { |
|
|
rows = rows.filter(r => String(r.data_name ?? '') === sel) |
|
|
} |
|
|
const series = buildSeriesFromLeaderboard(rows) |
|
|
const option = getChartOption(series, isDarkChart.value) |
|
|
chart.setOption(option, { notMerge: true }) |
|
|
} |
|
|
|
|
|
onMounted(() => { |
|
|
isDarkChart.value = window.matchMedia("(prefers-color-scheme: dark)").matches |
|
|
|
|
|
if (!chartRef.value) return |
|
|
chart = echarts.init(chartRef.value) |
|
|
|
|
|
renderChart() |
|
|
|
|
|
resizeHandler = () => chart && chart.resize() |
|
|
window.addEventListener('resize', resizeHandler) |
|
|
}) |
|
|
|
|
|
|
|
|
watch(leaderboard, () => { |
|
|
renderChart() |
|
|
}, { deep: true }) |
|
|
|
|
|
|
|
|
watch(selectedDataNameChart, () => { |
|
|
renderChart() |
|
|
}) |
|
|
|
|
|
|
|
|
watch(isDarkChart, (newVal) => { |
|
|
console.log('isDarkChart changed to:', newVal) |
|
|
renderChart() |
|
|
}) |
|
|
|
|
|
onBeforeUnmount(() => { |
|
|
if (resizeHandler) window.removeEventListener('resize', resizeHandler) |
|
|
if (themeObserver) themeObserver.disconnect() |
|
|
if (chart) { chart.dispose(); chart = null } |
|
|
}) |
|
|
</script> |
|
|
|
|
|
<template> |
|
|
<div ref="chartRef" class="w-2/5 h-[80vh] mx-auto"></div> |
|
|
</template> |
|
|
|
|
|
<style scoped></style> |