TeleAI-AI-Flow's picture
Upload Chart.vue
f756260 verified
<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()
// 接受一个可选 prop:autoShowSeries(数组),用于指定哪些模型类型在图表加载时自动显示。
// 例如:<Chart :autoShowSeries="['gemma-3','Hunyuan']" />
const props = defineProps({
autoShowSeries: { type: Array, default: () => [] }
})
// 把 leaderboard 转成 ECharts 所需的 series 数组(每个 model_series 一条线)
function buildSeriesFromLeaderboard(rows = []) {
const groups = {}
rows.forEach(r => {
// 按 model_type 分组;如果 model_type 未填则标记为 未知
// 优化:如果行上有 _isMoE 标记,则把其归为 "<model_type> (MoE)" 系列,且保留 model_series_base
const baseType = r.model_type || '未知'
const isMoE = !!r._isMoE
const seriesName = isMoE ? `${baseType} (MoE)` : baseType
if (!groups[seriesName]) groups[seriesName] = []
// 数据原本单位为 FLOPs (G),转换为 FLOPs(1 TFLOP = 1e12 FLOPs)用于对数尺度显示
const xRaw = parseFloat(r.BF16_TFLOPs) || 0
const x = xRaw > 0 ? xRaw * 1e12 / 1024 : 0
// const x = xRaw
const y = parseFloat(r.ic) || 0
const rank = Number(r.rank) || 0
groups[seriesName].push({ x, y, rank })
})
return Object.keys(groups).map(name => {
// 过滤掉 x<=0 的点(对数坐标轴不能显示 0 或负值),并按 x 轴值排序
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])
// 根据该系列的样本点数量适度放大 symbolSize,避免点太小难以点击
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
// 对数轴不能为 0 或负值,确保 min>0;并添加一些 padding
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) {
// small symmetric padding
// 如果只有单一数值,使用该值作为中心;但若该值为 0,则不要把区间提升到以 1 为中心(之前用 `yMin || 1` 会导致 0 -> 1)
const v = (yMin !== undefined ? yMin : 1)
if (v === 0) {
// 为 0 时给一个小的对称 padding(之后如果原始数据全为正,会被下限为 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
}
// 如果 yMin 变为负但原数据全为正,可以下限为 0
if (yMin < 0 && Math.min(...allY) >= 0) yMin = 0
}
// helper: 将整数指数转换为 Unicode 上标字符,例如 9 -> '⁹', -3 -> '⁻³'
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('')
}
// 可循环使用的视觉样式(颜色/符号/线型)
// 7 种颜色从红到紫,重复使用
const colors = ['#1F77B4', '#ff7f0e', '#2CA02C', '#D62728', '#9467BD', '#8C564B', '#E377C2', '#7F7F7F', '#BCBD22', '#17BECF']
const lineStyles = ['solid', 'solid', 'solid', 'solid', 'dash', 'solid', 'solid']
// 先构建不带颜色/符号的基础 series(rawSeries),后面会在排序后按顺序分配颜色和符号
const rawSeries = series.map(s => {
const baseSize = Number(s.symbolSize) || 8
return Object.assign({}, s, { symbolSize: baseSize, hoverAnimation: true })
})
// 构建 legend.selected 映射:只有 props.autoShowSeries 中的系列会默认显示,其他需手动点击 legend 打开
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)
})
}
// 按 modelTypeMapping 的定义顺序对 series 排序(使用来自 useLeaderboardData 的 modelTypeGroups)
const preferredOrder = (modelTypeGroups && Array.isArray(modelTypeGroups.value)) ? modelTypeGroups.value : []
const orderIndex = {}
preferredOrder.forEach((n, i) => { orderIndex[String(n)] = i })
// console.log('Preferred series order:', preferredOrder)
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
inOrder.sort((a, b) => orderIndex[String(a.name)] - orderIndex[String(b.name)])
// 其余的按名字字母序 (目前全部指定映射了)
rest.sort((a, b) => String(a.name).localeCompare(String(b.name)))
// console.log('Final series order:', inOrder.map(s => s.name), rest.map(s => s.name))
const finalSeries = [...inOrder, ...rest]
// 现在按 finalSeries 顺序分配颜色和符号,保证颜色顺序与 modelTypeMapping 一致
const finalSeriesStyled = finalSeries.map((s, idx) => {
const color = colors[idx % colors.length]
// 如果 series 名称包含 (MoE) 则使用星形
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')
// console.log(`Series "${s.name}" uses symbol "${symbolShape}"`)
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] : ''
// 显示 X 为 mantissa×10^n,并把指数用上标显示
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 || []
// 根据 selectedDataNameChart 过滤数据集(如果未选择或为 'all' 则使用全部)
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 //true | false
if (!chartRef.value) return
chart = echarts.init(chartRef.value)
// 首次渲染(如果数据尚未加载,后续会由 watcher 更新)
renderChart()
resizeHandler = () => chart && chart.resize()
window.addEventListener('resize', resizeHandler)
})
// 当 leaderboard 变化时重新渲染(不受 visibleColumns 影响)
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>