坐标轴是直角坐标系类型图表十分重要的组成部分。坐标系中横纵坐标轴以及刻度的作用下会产生一个数据空间。一般来说数据空间会根据数据的分布确定,数据应当合理地分布在数据空间中,不至于使数据数据空间有过多的空白。好的数据空间应当至少满足以下两个条件:
- 尽可能精确地拟合数据的分布,不至于浪费显示面积,也不应该使数据超出坐标轴显示。
- 数据空间应当根据数据或者图形本身的特点,体现数据的信息。
在这两个基本原则的基础上,可以衍生许多不同的价值取向。例如柱状图,矩形的高度代表数值的大小。但不同矩形之间高度比例,体现了不同组数据之间的差值,也是数据中一个很重要的信息。
例如下面三张年份-销量图,图一直观一看不同数据之间的差距很大。
如果 1963 的新增数据在 160 以上,在直觉上给人感觉:1959 至 1962 年市场低迷,1962 年销量暴涨,同比前一年翻番的感觉,而这个信息其实是不准确的。
所以,如果数据全部分布在 x 轴同一侧时,y 轴绘制的的起点一般都是 0(如果比较的基数不是 0 的情况下另当别论)。如果数据分布在 x 轴两侧,矩形的绘制起点也会是 0。
而折线图,在基础的数值之外,关注的是数据变化的趋势,这同样是数据中蕴含的信息。不恰当的 y 轴,有可能会使这种信息表达得特别不明显,甚至破坏这种信息,误导看图者。
例如上图,数据分布的范围比较小,直觉上销量数据在 170 左右波动,没有明显的趋势变化。但是如果经过处理后,将波动的特征放大,会发现数据在整体上,有先上升,后下降的趋势。
所以,在数据原始的分布区间离 0 特别远的时候,把 0 作为 y 轴的起点是不合适的。但是这种情况也并不绝对。数据本身的含义,有可能希望 y 轴就是从 0 开始。
坐标轴三要素
确定坐标轴有三个要素:
- 起点和终点(最大值和最小值)
- 刻度数
- 步长(刻度数之间的间距)
在知道最大值和最小值的情况下,刻度数和步长是可以彼此推算出来的。
Step =( max - min ) ÷ TickCount
一般来说,期望的刻度数是容易得出的。太密的刻度其实会让坐标轴显得很拥挤。AntV-G2 默认的数量是 5,我们就暂时取刻度数 TickCount 为 5。而数据的最大最小值,是可以轻易地通过遍历数据来获得。如果仅仅到这里为止,那么会发现我们已经可以根据每个数据的特点动态确定坐标轴了。但是结果并没有我们想的那么乐观。
例如折线图的一组数据,最大最小值分别为:24,102。那么根据上面的公示算出来的步长step = 15.6
,刻度数分别是:
24,39.6,55.2,70.8,86.4,102
很明显,这样直接计算得出来的刻度,可读性很差。所以在这一环节,步长的规范化是关键。
步长规范化
在计算出原始的步长后,我们希望能得出一个近似的,但是规范化的步长,例如 10 ,20, 25 这样的整数。可以通过下面的方法进行计算。
const getStandarInterval = (t: number) => {
if (t <= 0.1) {
t = 0.1;
} else if (t <= 0.2) {
t = 0.2;
} else if (t <= 0.25) {
t = 0.25;
} else if (t <= 0.5) {
t = 0.5;
} else if (t < 1) {
t = 1;
} else {
t = getStandarInterval(t / 10) * 10;
}
return t;
};
const rawTickInterval: number = (max - min) / tickCount;
// 计算数量级
mag = Math.pow(10, Math.floor(Math.log10(rawTickInterval)));
if (mag == rawTickInterval) {
mag = rawTickInterval;
} else {
mag = mag * 10;
}
tickInterval = Number((rawTickInterval / mag).toFixed(6));
//选取规范步长
const stepLen = getStandarInterval(tickInterval);
tickInterval = stepLen * mag;
首先归一化,我们给出一个标准步长的数组[0,1, 0.2, 0.25, 0.5, 1]
。
然后把计算原始步长的数量级,并且把原始步长归一化。例如 15.6 => 0.156,数量级为 100
接着把归一化后的步长标准化,0.156 => 0.2。
再将标准化后的步长,恢复到原来的数量级,标准步长stepLen = 0.2*100 = 20
。
在步长标准化时,逼近的规则不是近似,而是向上取整。因为缩小步长可能会导致在给定的刻度数内无法完整展示数据。
获取标准化的步长后,仅仅确定了三要素中的一个。
反推最大值、最小值和刻度数
获取了标准化步长后,并非万事大吉。如果我们直接使用原始数据的最大最小值进行坐标轴绘制,依然存在一定问题。例如原始数据最小值为 3.123,所有的刻度都会带着这样的小数。
一般来说,我们总是 0 这个点是在刻度中。即最大值最小值应该是步长的整数倍!
在确定坐标轴最大最小值时,我们就有了许多要考虑的要素:
- 数据是否全部分布在 x 轴的同一侧,即是否全部大于 0 或者小于 0。
- 如果数据全部在同一侧,x 轴是否需要从 0 开始绘制(最小值/最小值是否强制设为 0),柱状图一般需要从 0 开始。
无论如何,我们可以先确定最小值,最小值应该是一个刚好小于原始数据最小值(或者 0),且是步长的整数倍。确定最小值后,从最小值向上累加步长,直到刚好大于原始数据最大值。最大值也会是步长的整数倍。
let tempmin = 0;
if (min < 0) {
while (tempmin > min) {
tempmin -= tickInterval;
}
} else {
while (tempmin + tickInterval < min) {
tempmin += tickInterval;
}
}
min = tempmin;
let tickCount = 1;
while (tickCount * tickInterval + min < max) {
tickCount++;
}
max = tickCount * tickInterval + min;
这个过程中,顺便确定了真实的 tickCount,因为标准化步长时,对步长进行了适当放大,最后刻度数可能会小于给定的刻度数,为了避免图表上面空白过大,会按实际数据占位来确定最后的刻度数。这样就完成了一个简单的根据数据计算标准化刻度的过程。
在这个计算的过程中有很多地方可以需求进行调整,例如如果不介意最后计算得出的刻度数 tickCount 大于输入的 tickCount,更倾向于稍密集的刻度数,可以在获取标准化步长的过程中将向上取整改为取最近似的标准步长。还可以把标准化步长数组调整得更密集,能更好地逼近原始步长。还有是否从 0 开始计算等等。