PAYATHON 2026

如何让 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 轴上保持严格的整数精度。可以根据用途选择以下方案:

  1. 数列计算和 tooltip 继续使用 BigInt,图表坐标使用 log10 近似值。
  2. 超出安全范围后,按最大值归一化到 0 至 1。这样可以保留大致比例,但不能将图表标为数学精确。
  3. 使用 ECharts custom series,自行将 BigInt 映射到整数像素。映射时可以通过整数运算控制舍入,最终显示仍受画布分辨率限制。
  4. 使用支持任意精度坐标的自定义 SVG 或 Canvas 渲染器。屏幕像素数量有限,因此显示的仍是投影结果,无法呈现任意多位整数的全部精度。

实际使用时,可以将精确性的范围定义清楚:计算值和 tooltip 始终保持精确;线性坐标只在安全整数范围内标为精确;超出范围后,将图表明确标记为对数图或近似图。

备注:内容仅供参考。