产品里绘制了大量的图表,柱状图有单轴的也有双轴的,但是双轴的图如果使用echarts默认的绘制,发现两边的坐标并没有对齐,如果绘制辅助线,会发现grid区域有大量错开的辅助线,图形整体就很乱,产品不止一次让我想办法,为此多掉了好多头发,今天就来聊聊数学的重要性,没错是数学!
- 现象的产生
当两个Y轴对应的数据差距很大,比如一个是总量,一个是占比,单纯设置两个y轴的splitNumber并不能起到将两个y轴分成同样份数的效果,因为我们设置的splitNumber只是一个建议的值,但实际绘制中echarts并不会完全按照splitNumber来分割y轴,而是根据对应的数据自己选择最美观的绘制区间,但这个区间是对应一组数据来说的,不会去配合另外的数据来使他们对齐。
- 解决方法:上图是echarts官方示例中的一个,原图中给其两个y轴都设置了最大值Max,最小值Min,另外坐标轴还有个属性interval(每个刻度的长度),那么这三个属性就是脱坑的关键,下面上正菜:
- 确定数据的max和min
在实际开发中不可能写死最大值和最小值,因为实际数据的大小并不可控而且封装成组件时,这样写死也会造成组件的实用性降低,所以我们要根据实际获取到的数据来设置max和min, 那么就先要获取到数据的max和min
calMax(arr) { // arr是传入的series
const yAxisData = [[], []] // 连个y轴,用两个数组将不同y轴对应数据分开
const maxArr = [] // 用来缓存计算出来的所有Y轴的最大值
arr.forEach(item => {
if (item.yAxisIndex === 0) { // 根据数据对应的y轴的下标将数据分别缓存在yAxisData中
yAxisData[0] = yAxisData[0].concat(item.data)
}
if (item.yAxisIndex === 1) {
yAxisData[1] = yAxisData[1].concat(item.data)
}
})
yAxisData.forEach((item, index) => {
let max = Number(item[0])
item.forEach((i, index) => {
if (Number(i) && Number(i) > max) {
max = Number(i)
}
})
maxArr[index] = max
})
return maxArr
},
最小值参照上面,一样的操作
- 确定我们期望绘制的max和min,用差值除以你需要的splitNumber来定义interval
通过 以上一同操作获取到数据的最大值,下面就开始重头戏,设置我们期望绘制的max和min,这就用到了数学,直接上代码
const max = this.calMax(this.baseOptions.series)
const min = this.calMin(this.baseOptions.series)
this.baseOptions.yAxis.forEach((item, index) => {
if (options.yAxis[index] && !options.yAxis[index].max) { // 做个截断这样最大值和最小值也可以是手动传入的,例如y轴是月份等固定的数,可以手动传入
item.max = max[index]
item.min = min[index]
if (item.max > 0) {
const maxDigit = Math.floor(Math.log(Math.abs(item.max)) / Math.log(10)) - 1 // 根据求最大值以10为底的对数计算最大值得位数(不用管是不是小数,都是可以实现的)
// Math没有以10为底的对数求法,这里用了对数的换底公式
// 减1是因为省略了一步,如果直接用位数继续下面的计算,求出来的最大值可能会比数据真实的最大值大太多,图形一般会集中分布在grid的下方
// 减去一位再进行计算,计算所得的最大值一般会比数据的最大值低一位,这样图形整体就比较丰满,一般距离图表顶部一格左右
const maxMultiple = Math.pow(10, maxDigit)
// 用pow方法记录我们需要最终精确地位数
tem.max = Math.ceil((Math.abs(item.max) * 1.2) / maxMultiple) * maxMultiple
// 先将数据最大值放大1.2倍(根据自己的需要),在利用之前的记录的精度对齐向上取整,这样就能获取到你想要的数据(10, 100, 5000, 0.3, 0.005, ...)
} else {
item.max = 0
// 这里如果最大值<= 0,我直接取0, 根据需求自己确定
}
if (item.min >= 0) {
item.min = 0
} else {
const minDigit = Math.floor(Math.log(-item.min) / Math.log(10)) - 1
const minMultiple = Math.pow(10, minDigit)
tem.min = -Math.ceil((Math.abs(item.min) * 1.2) / minMultiple) * minMultiple
// 这里只给最小值为负时设置的算法,因为为正时直接赋值为0,图形会更好
}
// 设置Interval,我这里所有的splitNumber都默认为5,也可以选择自己传入,一幅丰满且适用于多种多样是坐标系图就完成了
item.interval = (item.max - item.min) / 5
}
}
以上方法基本你可以解决所有双轴问题,至少我们项目中现在很nice,夸一下自己,一开始我们选择了分层,根据最大值的范围,10, 100, 1000,…分出一个个的范围,最终觉得枚举就算列完了所有情况。但代码也不好维护,最终我只能默默的掏出了珍藏了7,8年的高中数学,终于完成了。
最后,希望还在采坑的猿们可以试试,应该能够帮到你,有bug请留言,毕竟自己想出来的,不一定没有bug,谢谢