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