如何让 ECharts 同时准确展示 BigInt 与 log10 数据?
结论
ECharts 的数值轴使用 JavaScript Number,具体处理方式取决于数列是否超出安全整数范围:
- 如果 Collatz 数列中的所有值都不超过
Number.MAX_SAFE_INTEGER,可以将BigInt转成Number,绘制坐标精确的线性图。 - 超过安全整数范围后,ECharts 无法直接绘制任意精度的线性坐标。原始
BigInt可以继续用于计算和 tooltip,图表坐标则需要转换为近似值。 - 绘制对数图时,可以使用
log10(BigInt)的近似值。转换时只读取开头若干位数字,无需先将完整的BigInt转成Number。 - 以
27为起始值时,数列最大值是9232,远未超出安全整数范围,可以直接使用Number(value)。当前图形与线性图不同,是因为数据提前执行了log10,与精度丢失无关。
将 log10(n) 画在线性数值轴上,再把刻度标签格式化为 n,只会改变标签文字,点的位置仍按对数值计算,原来的线性几何关系不会恢复。
为什么当前图表和线性图不同
当前代码传给 ECharts 的数据是:
const nums = spisok.map(graphLogSize);
例如:
27 -> log10(27) ≈ 1.431
9232 -> log10(9232) ≈ 3.965
图中实际绘制的是 log10(n),数值之间的高度比例已经发生变化。后面的 axisLabel.formatter 只控制标签如何显示,不会改变数据点的位置。
要绘制线性图,传给 series.data 的必须是原数值:
const nums = spisok.map(value => Number(value));
这种写法要求每个值都是安全整数。
分别创建线性图和对数图
先在页面中准备两个容器:
<div id="linearChart" class="chart-block"></div>
<div id="logChart" class="chart-block"></div>
<p id="linearStatus"></p>
分别初始化两个图表:
const linearChart = echarts.init(
document.getElementById('linearChart'),
'dark'
);
const logChart = echarts.init(
document.getElementById('logChart'),
'dark'
);
const linearStatus = document.getElementById('linearStatus');
const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
通用 tooltip
原始 BigInt 无需放入 series.data。将数列保留在闭包中,tooltip 可以按索引读取对应的原始值:
function toolTipShorten(value) {
const s = value.toString();
if (s.length <= 30) {
return s;
}
return `${s.slice(0, 12)}...${s.slice(-12)}`;
}
function operationOf(value) {
if (value === 1n) return 'Stop';
return value % 2n === 0n ? '/ 2' : '* 3 + 1';
}
function createTooltip(sequence) {
return {
trigger: 'axis',
formatter(params) {
const index = params[0].dataIndex;
const value = sequence[index];
return [
`Step: ${index}`,
`Value: ${toolTipShorten(value)}`,
`Operation: ${operationOf(value)}`
].join('<br>');
}
};
}
这样可以保留精确的 BigInt 计算结果,同时只向 ECharts 传入能够绘制的 Number。
精确线性图
绘图前,先检查整个数列能否安全转换:
function canUseExactNumber(sequence) {
return sequence.every(value =>
value >= 0n && value <= MAX_SAFE_BIGINT
);
}
通过检查后再绘图:
function renderExactLinearChart(sequence) {
if (!canUseExactNumber(sequence)) {
linearChart.clear();
linearChart.setOption({
title: {
text: 'Linear chart',
subtext: '数列超过 Number.MAX_SAFE_INTEGER,无法用 ECharts 数值轴精确绘制'
},
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: []
});
linearStatus.textContent =
'原始 BigInt 计算结果仍然精确,但当前数值范围无法转换为精确的 ECharts 坐标。';
return false;
}
linearStatus.textContent = '';
const data = sequence.map(value => Number(value));
linearChart.setOption({
title: {
text: 'Exact linear chart'
},
tooltip: createTooltip(sequence),
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
type: 'slider',
start: 0,
end: 10
}
],
xAxis: {
type: 'category',
boundaryGap: false,
data: sequence.map((_, index) => index)
},
yAxis: {
type: 'value',
axisLabel: {
formatter(value) {
if (Math.abs(value) < 1e6) {
return String(Math.round(value));
}
return value.toExponential(2);
}
}
},
series: [
{
type: 'line',
data,
showSymbol: true,
symbolSize: 6
}
]
}, true);
return true;
}
从 27 开始的 Collatz 数列可以走这个分支,因为所有值都远小于 Number.MAX_SAFE_INTEGER。
支持任意长度 BigInt 的对数图
不要使用下面这种转换方式:
Math.log10(Number(value))
value 很大时,Number(value) 会丢失有效数字,甚至得到 Infinity。可以改为读取十进制字符串,并据此计算近似对数:
function log10BigInt(value) {
if (value <= 0n) {
throw new RangeError('log10BigInt only accepts positive BigInt values');
}
const s = value.toString();
const significantDigits = 15;
if (s.length <= significantDigits) {
return Math.log10(Number(s));
}
const leading = Number(s.slice(0, significantDigits));
return (
Math.log10(leading) +
(s.length - significantDigits)
);
}
返回值仍然是 Number,不具备任意精度,但可以保留大约前 15 位有效数字,也不会因为完整整数过大而溢出。
格式化对数轴标签时,同样不必计算完整的 10 ** value,只需计算尾数:
function formatLogAxis(logValue) {
if (!Number.isFinite(logValue)) {
return '';
}
const exponent = Math.floor(logValue);
const mantissa = 10 ** (logValue - exponent);
if (Math.abs(mantissa - 1) < 1e-10) {
return `1e${exponent}`;
}
return `${mantissa.toFixed(2)}e${exponent}`;
}
完整的对数图配置如下:
function renderLogChart(sequence) {
const data = sequence.map(log10BigInt);
logChart.setOption({
title: {
text: 'Logarithmic chart'
},
tooltip: createTooltip(sequence),
toolbox: {
feature: {
dataZoom: {
yAxisIndex: 'none'
},
restore: {},
saveAsImage: {}
}
},
dataZoom: [
{
type: 'inside',
start: 0,
end: 100
},
{
type: 'slider',
start: 0,
end: 10
}
],
xAxis: {
type: 'category',
boundaryGap: false,
data: sequence.map((_, index) => index)
},
yAxis: {
type: 'value',
name: 'log10(n)',
axisLabel: {
formatter: formatLogAxis
}
},
series: [
{
type: 'line',
data,
showSymbol: true,
symbolSize: 6
}
]
}, true);
}
运行时调用两个渲染函数:
runBtn.addEventListener('click', () => {
const input = inp.value.trim();
if (!input) {
return;
}
const start = BigInt(input);
const sequence = collatzBigInt(start);
renderExactLinearChart(sequence);
renderLogChart(sequence);
});
超过安全整数范围时怎么办
Number.MAX_SAFE_INTEGER 的值是:
9007199254740991
超出这个范围后,即使 Number(bigint) 没有返回 Infinity,两个相邻整数也可能被转换成同一个 Number:
Number(9007199254740992n) === Number(9007199254740993n);
// true
任意大小的 BigInt 无法在 ECharts 的普通 value 轴上保持严格的整数精度。可以根据用途选择以下方案:
- 数列计算和 tooltip 继续使用
BigInt,图表坐标使用log10近似值。 - 超出安全范围后,按最大值归一化到
0至1。这样可以保留大致比例,但不能将图表标为数学精确。 - 使用 ECharts
customseries,自行将BigInt映射到整数像素。映射时可以通过整数运算控制舍入,最终显示仍受画布分辨率限制。 - 使用支持任意精度坐标的自定义 SVG 或 Canvas 渲染器。屏幕像素数量有限,因此显示的仍是投影结果,无法呈现任意多位整数的全部精度。
实际使用时,可以将精确性的范围定义清楚:计算值和 tooltip 始终保持精确;线性坐标只在安全整数范围内标为精确;超出范围后,将图表明确标记为对数图或近似图。
备注:内容仅供参考。
