资讯详情 坐标轴刻度别靠硬凑:用一个小算法画出 0、20、40、60、80、100
📅 2026/10/25 21:24:23
数据范围是 12.8 到 88.4简单等分会得到一串不好读的小数。把所有刻度随手改成整数也不行间隔不等、重复数字或基线错位会让图看起来规整读数却失真。我会先问图表任务是比较量级、观察局部波动还是要让人精确读值柱图通常应保留零基线局部趋势图可以采用更紧的范围但要明确显示坐标不偷偷夸大变化。“漂亮”首先得有规则D3 的 ticks 文档说明刻度值常采用 1、2、5 乘 10 的幂数量是近似目标。下面给出一个更小的向上取整步长算法不把它称为完整的扩展 Wilkinson 实现。D3 刻度文档刻度不能统一保留十位小数01e-12这样的范围会被全部舍成零。下面用Fraction精确选择步长再构造Decimal刻度计算过程不转回二进制浮点数。这个教学函数接受int、float、十进制字符串和Decimal返回Decimal列表。非零输入的十进制数量级指数限定为 −100 到 100输入最多含 100 位小数目标间隔数是 11000 的整数超出范围明确报错。float按它的str()表示解释差值若在传入前已经丢失函数无法恢复很窄的范围应优先传字符串或Decimal。from decimal import Decimal, InvalidOperation from fractions import Fraction def decimal_bound(value): if isinstance(value, bool) or not isinstance(value, (int, float, str, Decimal)): raise ValueError(a decimal number is required) try: value Decimal(str(value)) except InvalidOperation as error: raise ValueError(invalid decimal number) from error if not value.is_finite(): raise ValueError(finite bounds required) if value.as_tuple().exponent -100 or ( value ! 0 and not -100 value.adjusted() 100): raise ValueError(bounds outside the teaching domain) return value def nice_ticks(low, high, intervals5): if isinstance(intervals, bool) or not isinstance(intervals, int) or not 1 intervals 1000: raise ValueError(intervals must be an integer in [1, 1000]) low, high Fraction(decimal_bound(low)), Fraction(decimal_bound(high)) if low high: raise ValueError(low must not exceed high) if low high: # Explicit constant-axis padding policy, not a universal default. padding max(Fraction(1), abs(low) / 100) low, high low-padding, highpadding raw (high-low) / intervals exponent len(str(raw.numerator))-len(str(raw.denominator)) magnitude Fraction(10) ** exponent if raw magnitude: exponent - 1 magnitude / 10 unit next(v for v in (1, 2, 5, 10) if v*magnitude raw) step unit * magnitude start low // step stop -((-high) // step) # String construction is exact; no fixed decimal rounding or context division. return [Decimal(f{i*unit}e{exponent}) for i in range(start, stop1)] ticks nice_ticks(12.8, 88.4) assert ticks [0, 20, 40, 60, 80, 100] tiny nice_ticks(0, 1e-12) assert tiny [Decimal(v) for v in (0, 2e-13, 4e-13, 6e-13, 8e-13, 1e-12)] cases [ (12.8, 88.4, 5), (0, 1e-12, 5), (0.1, 0.3, 5), (1.0000000000001, 1.0000000000009, 5), (-0.37, -0.11, 5), (-1e-12, 1e-12, 5), (7, 7, 5), (0, 0, 5), (0.001, 0.001, 5), (1e16, 1e162, 5), (0, 1e-100, 1000), (1e100, 1.1e100, 5)] for low, high, count in cases: result nice_ticks(low, high, count) assert 2 len(result) count3 assert result[0] decimal_bound(low) and result[-1] decimal_bound(high) gaps [Fraction(b)-Fraction(a) for a, b in zip(result, result[1:])] assert gaps[0] 0 and len(set(gaps)) 1 # Strict order and exact equal spacing. invalid [(1, 0, 5), (0, 1, 0), (0, 1, -1), (0, 1, True), (0, 1, 2.5), (0, 1, 1001), (0, float(inf), 5), (float(nan), 1, 5), (True, 1, 5), (None, 1, 5), (bad, 1, 5), (0, 1e-101, 5), (0, 1e101, 5), (0, 5e-324, 5)] for args in invalid: try: nice_ticks(*args) except ValueError: pass else: raise AssertionError((unsupported input accepted, args)) print(main:, [format(v, f) for v in ticks]) print(tiny:, [str(v) for v in tiny]) print(boundary cases:, len(cases), accepted;, len(invalid), rejected as specified)主例步长仍是 20得到六条刻度微小范围则得到0、2e-13、4e-13、6e-13、8e-13、1e-12不会再合并成零。断言还检查了普通小数、窄范围、负数、跨零、常量、大基数小跨度以及约定输入域的两端。每组都核对覆盖边界、严格递增和精确等距而不是只检查主例。目标间隔数依然是近似值不保证所有范围都正好六条刻度。零跨度时这里采用max(1, |值|×1%)留白很小的常量也可能被扩到跨零范围。这只是写明的一种策略业务图表应按单位和任务另定留白不能把它当成唯一正确的默认值。生成刻度与摆放标签是两项工作数值间距正确不代表文字就不会重叠。百万与千的缩写、负数符号、小数位、纵轴旋转和窄屏宽度都需要处理。可以减少显示标签却不能改变刻度对应的真实位置省略的单位也应放在轴标题或图注里。Decimal可用format(value, f)生成标签。若前端只接受 JavaScriptNumber直接转回浮点数仍可能把大基数下的微小差值合并可以考虑显示相对原点的偏移或核对图表引擎的数值能力。这里没有验证这层适配精确生成刻度不等于任意前端都能精确摆放它们。跨图比较时自动缩放还可能造成另一个误读两张图的柱高一样但一个上限 100、另一个上限 1000。若任务是比总量使用共同范围若任务是看各自趋势则提示范围不同。本文只运行数值算法没有验证 D3/ECharts 渲染、文本测量或无障碍体验。也没有“阅读效率提升 300%”的用户实验。好的刻度不需要这种装饰数字读者能沿着等距标记复算已经是它的主要价值。延伸阅读D3 刻度文档W3C 非文本对比度解释