1. 概述
在本文中,我们将研究如何在图表中自动布局网格线(gridline)的间隔。该算法特别适用于柱状图(bar chart)中的网格线设置,能够帮助我们选择“视觉上友好”的间隔数值,使得图表清晰易读。
2. 网格线间隔的放置策略
在之前的文章《为 y 轴选择线性刻度》中,我们讨论了如何在线图(line chart)中确定轴上的刻度位置。而本文则关注一个相关的算法,它更适合用于柱状图中网格线的间隔设定。
以一个表示分布的图表为例,比如某股票连续 10 天的每日价格图:
我们希望确定 y 轴上刻度的合理数量与位置。目标是找到一个合适的间隔值,使得观察值之间的相对位置能够清晰呈现。
3. 需要避免的常见错误
分布的范围是我们首先要考虑的约束条件。在这个范围内,我们需要大致均匀地布置网格线。但同时,我们也要避免两个常见错误:
✅ 太少的网格线,会导致图表信息不充分,难以判断数值之间的相对关系:
❌ 太多的网格线,则可能导致刻度重叠,视觉混乱:
4. 平衡才是关键
我们希望网格线既覆盖图表区域,又留有足够空间,让刻度与数值清晰可读。如果处理得当,最终效果如下图所示:
这样的图表既具有信息量,又不会显得拥挤。
5. 什么样的数值是“友好”的?
接下来,我们需要确定哪些数值适合作为网格线的刻度点。
视觉认知研究表明,1、2 和 5 的倍数最容易被人类理解。例如,左边的数值比右边的更“友好”:
友好 | 不友好 |
---|---|
1 | 1.7 |
2 | 3 |
5 | 4.9 |
10 | 9 |
这一原则也适用于不同数量级:
友好 | 不友好 |
---|---|
0.1 | 0.17 |
20 | 30 |
0.05 | 0.49 |
1000 | 900 |
这意味着我们可以建立一个与尺度无关的“友好数值”列表。只要我们在 [1,10] 区间内定义好一个列表,就可以通过乘以 10 的幂次来扩展到任意数量级。
例如:如果我们定义了一个 [1, 2, 2.5, 3, 5, 7, 7.5, 10] 的列表,就可以根据需要缩放为 10x、100x、0.1x 等。
6. 算法实现
我们可以设计一个算法,根据输入的数值范围和最大刻度数,自动选择合适的网格线间隔。
输入参数:
dataRange
: 数据范围(如最大值 - 最小值)maxTicks
: 用户期望的最大刻度数
输出结果:
- 一组“友好”的刻度值(tick marks)
算法步骤:
- 计算理想间隔:
idealInterval = dataRange / maxTicks
- 找到最接近
idealInterval
的数量级(即 10 的幂) - 在预定义的“友好数值”表中查找最接近的间隔值
- 根据这个间隔值生成刻度点列表
示例代码(Java):
public class NiceGridlines {
private static final double[] NICE_VALUES = {1, 2, 2.5, 3, 5, 7, 7.5, 10};
public static double calculateNiceInterval(double range, int maxTicks) {
double rawInterval = range / maxTicks;
double magnitude = Math.pow(10, Math.floor(Math.log10(rawInterval)));
double relativeValue = rawInterval / magnitude;
// Find the closest nice value
for (int i = 0; i < NICE_VALUES.length; i++) {
if (NICE_VALUES[i] >= relativeValue) {
return NICE_VALUES[i] * magnitude;
}
}
return NICE_VALUES[NICE_VALUES.length - 1] * magnitude;
}
public static List<Double> generateTicks(double min, double max, double interval) {
List<Double> ticks = new ArrayList<>();
double current = Math.ceil(min / interval) * interval;
while (current <= max) {
ticks.add(current);
current += interval;
}
return ticks;
}
public static void main(String[] args) {
double min = 0;
double max = 100;
int maxTicks = 5;
double interval = calculateNiceInterval(max - min, maxTicks);
List<Double> ticks = generateTicks(min, max, interval);
System.out.println("Generated ticks: " + ticks);
}
}
示例输出:
Generated ticks: [20.0, 40.0, 60.0, 80.0, 100.0]
算法流程图示意:
7. 小结
本文介绍了一个用于自动选择“友好”网格线间隔的算法。该算法基于对人类视觉认知的理解,通过预定义的“友好数值”表,结合输入数据范围与最大刻度数,自动计算出最佳间隔,从而提升图表的可读性与视觉体验。