feat(report): 优化报告图表并引入对数坐标轴

本次提交对报告页面的 ECharts 图表进行了多项视觉和可用性优化,以提升数据可读性和展示效果。

主要变更包括:

1.  **饼图优化**:
    -   统一将图例(Legend)固定在图表顶部,以获得更一致和整洁的布局。
    -   增大了饼图的半径,使其在视觉上更突出。

2.  **Token 对比图重构**:
    -   从垂直条形图改为水平条形图,更利于展示较长的标签。
    -   将数值轴(现为 X 轴)改为对数(log)坐标轴,有效解决了 Token 数量差异巨大时的显示问题,让小数值也能清晰可见。
    -   为对数轴处理了 0 值的情况,并在提示框中恢复显示原始值。
    -   数据缩放(dataZoom)也相应调整为在 Y 轴上进行。

3.  **其他条形图**:
    -   增加了条形的宽度,增强了视觉冲击力。
This commit is contained in:
minecraft1024a
2025-11-30 14:16:50 +08:00
parent 474f86af54
commit dd12c441a9

View File

@@ -313,9 +313,6 @@ document.addEventListener('DOMContentLoaded', function () {
value: providerCostData.data[idx] value: providerCostData.data[idx]
})); }));
const itemCount = pieData.length;
const useBottomLegend = itemCount > 8;
chart.setOption({ chart.setOption({
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
@@ -330,20 +327,17 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
legend: { legend: {
type: 'scroll', type: 'scroll',
orient: useBottomLegend ? 'horizontal' : 'vertical', orient: 'horizontal',
right: useBottomLegend ? 'center' : 10, left: 'center',
top: useBottomLegend ? 'auto' : 'middle', top: 0,
bottom: useBottomLegend ? 10 : 'auto', width: '90%',
left: useBottomLegend ? 'center' : 'auto',
width: useBottomLegend ? '90%' : '30%',
height: useBottomLegend ? 'auto' : '80%',
icon: 'circle', icon: 'circle',
itemWidth: 8, itemWidth: 8,
itemHeight: 8, itemHeight: 8,
itemGap: useBottomLegend ? 12 : 8, itemGap: 12,
textStyle: { textStyle: {
fontSize: 10, fontSize: 10,
width: useBottomLegend ? 70 : 80, width: 80,
overflow: 'truncate', overflow: 'truncate',
ellipsis: '...' ellipsis: '...'
}, },
@@ -356,8 +350,8 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['35%', '60%'], radius: ['45%', '70%'],
center: useBottomLegend ? ['50%', '40%'] : ['35%', '50%'], center: ['50%', '55%'],
avoidLabelOverlap: true, avoidLabelOverlap: true,
itemStyle: { itemStyle: {
borderColor: '#fff', borderColor: '#fff',
@@ -391,9 +385,6 @@ document.addEventListener('DOMContentLoaded', function () {
value: moduleCostData.data[idx] value: moduleCostData.data[idx]
})); }));
const itemCount = pieData.length;
const useBottomLegend = itemCount > 8;
chart.setOption({ chart.setOption({
tooltip: { tooltip: {
trigger: 'item', trigger: 'item',
@@ -408,20 +399,17 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
legend: { legend: {
type: 'scroll', type: 'scroll',
orient: useBottomLegend ? 'horizontal' : 'vertical', orient: 'horizontal',
right: useBottomLegend ? 'center' : 10, left: 'center',
top: useBottomLegend ? 'auto' : 'middle', top: 0,
bottom: useBottomLegend ? 10 : 'auto', width: '90%',
left: useBottomLegend ? 'center' : 'auto',
width: useBottomLegend ? '90%' : '30%',
height: useBottomLegend ? 'auto' : '80%',
icon: 'circle', icon: 'circle',
itemWidth: 8, itemWidth: 8,
itemHeight: 8, itemHeight: 8,
itemGap: useBottomLegend ? 12 : 8, itemGap: 12,
textStyle: { textStyle: {
fontSize: 10, fontSize: 10,
width: useBottomLegend ? 70 : 80, width: 80,
overflow: 'truncate', overflow: 'truncate',
ellipsis: '...' ellipsis: '...'
}, },
@@ -434,8 +422,8 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['35%', '60%'], radius: ['45%', '70%'],
center: useBottomLegend ? ['50%', '40%'] : ['35%', '50%'], center: ['50%', '55%'],
avoidLabelOverlap: true, avoidLabelOverlap: true,
itemStyle: { itemStyle: {
borderColor: '#fff', borderColor: '#fff',
@@ -545,7 +533,7 @@ document.addEventListener('DOMContentLoaded', function () {
borderRadius: [0, 6, 6, 0] borderRadius: [0, 6, 6, 0]
} }
})), })),
barMaxWidth: 20, barMaxWidth: 40,
emphasis: { emphasis: {
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.3)' } itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.3)' }
} }
@@ -576,6 +564,10 @@ document.addEventListener('DOMContentLoaded', function () {
const chart = echarts.init(tokenCompContainer); const chart = echarts.init(tokenCompContainer);
chartInstances[`tokenComparisonChart_${period_id}`] = chart; chartInstances[`tokenComparisonChart_${period_id}`] = chart;
// 处理数据,避免 log 轴报错 (0值转为1)
const inputData = tokenCompData.input_tokens.map(v => v < 1 ? 1 : v);
const outputData = tokenCompData.output_tokens.map(v => v < 1 ? 1 : v);
chart.setOption({ chart.setOption({
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
@@ -588,34 +580,36 @@ document.addEventListener('DOMContentLoaded', function () {
formatter: function(params) { formatter: function(params) {
let result = params[0].name + '<br/>'; let result = params[0].name + '<br/>';
params.forEach(p => { params.forEach(p => {
// 恢复原始值显示
const rawValue = p.value === 1 ? 0 : p.value;
const total = tokenCompData.input_tokens.reduce((a, b) => a + b, 0) + const total = tokenCompData.input_tokens.reduce((a, b) => a + b, 0) +
tokenCompData.output_tokens.reduce((a, b) => a + b, 0); tokenCompData.output_tokens.reduce((a, b) => a + b, 0);
const pct = total > 0 ? ((p.value / total) * 100).toFixed(1) : '0.0'; const pct = total > 0 ? ((rawValue / total) * 100).toFixed(1) : '0.0';
result += `${p.marker} ${p.seriesName}: ${p.value.toLocaleString()} tokens (${pct}%)<br/>`; result += `${p.marker} ${p.seriesName}: ${rawValue.toLocaleString()} tokens (${pct}%)<br/>`;
}); });
return result; return result;
} }
}, },
legend: { legend: {
data: ['输入Token', '输出Token'], data: ['输入Token', '输出Token'],
top: 10, top: 0,
icon: 'circle', icon: 'circle',
itemWidth: 10, itemWidth: 10,
itemHeight: 10 itemHeight: 10
}, },
grid: { grid: {
left: '3%', left: '3%',
right: '4%', right: needsZoom ? '8%' : '4%',
bottom: needsZoom ? '15%' : '3%', bottom: '8%',
top: 50, top: 30,
containLabel: true containLabel: true
}, },
dataZoom: needsZoom ? [ dataZoom: needsZoom ? [
{ {
type: 'slider', type: 'slider',
xAxisIndex: 0, yAxisIndex: 0,
height: 20, right: 5,
bottom: 5, width: 20,
start: 0, start: 0,
end: Math.min(100, Math.round(10 / itemCount * 100)), end: Math.min(100, Math.round(10 / itemCount * 100)),
handleSize: '100%', handleSize: '100%',
@@ -624,42 +618,54 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
{ {
type: 'inside', type: 'inside',
xAxisIndex: 0, yAxisIndex: 0,
zoomOnMouseWheel: 'shift', zoomOnMouseWheel: 'shift',
moveOnMouseMove: true moveOnMouseMove: true,
moveOnMouseWheel: true
} }
] : [], ] : [],
xAxis: { xAxis: {
type: 'log',
min: 1,
logBase: 10,
name: 'Token数量 (对数)',
nameTextStyle: { fontSize: 11, fontWeight: 'bold' },
axisLabel: {
fontSize: 10,
hideOverlap: true,
formatter: function(value) {
if (value === 1) return '0';
if (value >= 1000000) return (value / 1000000).toFixed(0) + 'M';
if (value >= 1000) return (value / 1000).toFixed(0) + 'k';
return value;
}
},
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.05)' } }
},
yAxis: {
type: 'category', type: 'category',
data: tokenCompData.labels.map(l => l.length > 20 ? l.substring(0, 20) + '...' : l), data: tokenCompData.labels.map(l => l.length > 20 ? l.substring(0, 20) + '...' : l),
axisLabel: { axisLabel: {
fontSize: 9, fontSize: 9,
rotate: itemCount > 6 ? 30 : 0,
interval: 0 interval: 0
}, },
axisTick: { show: false } axisTick: { show: false },
}, axisLine: { show: false }
yAxis: {
type: 'value',
name: 'Token数量',
nameTextStyle: { fontSize: 11, fontWeight: 'bold' },
axisLabel: { fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(0, 0, 0, 0.05)' } }
}, },
series: [ series: [
{ {
name: '输入Token', name: '输入Token',
type: 'bar', type: 'bar',
data: tokenCompData.input_tokens, data: inputData,
itemStyle: { color: '#FF9800', borderRadius: [6, 6, 0, 0] }, itemStyle: { color: '#FF9800', borderRadius: [0, 6, 6, 0] },
barMaxWidth: 25 barMaxWidth: 30
}, },
{ {
name: '输出Token', name: '输出Token',
type: 'bar', type: 'bar',
data: tokenCompData.output_tokens, data: outputData,
itemStyle: { color: '#4CAF50', borderRadius: [6, 6, 0, 0] }, itemStyle: { color: '#4CAF50', borderRadius: [0, 6, 6, 0] },
barMaxWidth: 25 barMaxWidth: 30
} }
], ],
animation: true, animation: true,
@@ -684,8 +690,6 @@ document.addEventListener('DOMContentLoaded', function () {
value: providerReqData.data[idx] value: providerReqData.data[idx]
})); }));
const itemCount = pieData.length;
const useBottomLegend = itemCount > 8;
const reqColors = ['#9C27B0', '#E91E63', '#F44336', '#FF9800', '#FFC107', '#FFEB3B', '#CDDC39', '#8BC34A', '#4CAF50', '#009688']; const reqColors = ['#9C27B0', '#E91E63', '#F44336', '#FF9800', '#FFC107', '#FFEB3B', '#CDDC39', '#8BC34A', '#4CAF50', '#009688'];
chart.setOption({ chart.setOption({
@@ -702,20 +706,17 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
legend: { legend: {
type: 'scroll', type: 'scroll',
orient: useBottomLegend ? 'horizontal' : 'vertical', orient: 'horizontal',
right: useBottomLegend ? 'center' : 10, left: 'center',
top: useBottomLegend ? 'auto' : 'middle', top: 0,
bottom: useBottomLegend ? 10 : 'auto', width: '90%',
left: useBottomLegend ? 'center' : 'auto',
width: useBottomLegend ? '90%' : '30%',
height: useBottomLegend ? 'auto' : '80%',
icon: 'circle', icon: 'circle',
itemWidth: 8, itemWidth: 8,
itemHeight: 8, itemHeight: 8,
itemGap: useBottomLegend ? 12 : 8, itemGap: 12,
textStyle: { textStyle: {
fontSize: 10, fontSize: 10,
width: useBottomLegend ? 70 : 80, width: 80,
overflow: 'truncate', overflow: 'truncate',
ellipsis: '...' ellipsis: '...'
}, },
@@ -728,8 +729,8 @@ document.addEventListener('DOMContentLoaded', function () {
}, },
series: [{ series: [{
type: 'pie', type: 'pie',
radius: ['35%', '60%'], radius: ['45%', '70%'],
center: useBottomLegend ? ['50%', '40%'] : ['35%', '50%'], center: ['50%', '55%'],
avoidLabelOverlap: true, avoidLabelOverlap: true,
itemStyle: { itemStyle: {
borderColor: '#fff', borderColor: '#fff',
@@ -834,7 +835,7 @@ document.addEventListener('DOMContentLoaded', function () {
borderRadius: [0, 6, 6, 0] borderRadius: [0, 6, 6, 0]
} }
})), })),
barMaxWidth: 18, barMaxWidth: 30,
emphasis: { emphasis: {
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.3)' } itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.3)' }
} }