先说说背景. 公司的出库零件数趋势以折线图展示. 然后有这样一个需求, 就是在折线图上以横线的方式标出当前时间点的出库预期值. 既然是预期值, 那么即有可能会小于或等于实际值, 也有可能会大于实际值. 因此该需求可以用 markLine 简单实现一下:
// goal 为目标值, extent 中能够拿到所有折线图 series 的最大值和最小值
{
yAxis: {
max: extent => extent.max > goal ? extent.max : goal
},
series: {
markLine: {
data: [{
yAxis: goal
}]
}
}
}
效果 🔽
这样已经实现了功能! 然而这就可以交差了吗? 当然不是, 我们注意看上面截图的效果,纵轴最上面的一个坐标值是 13330, 而不是 15000。也就是说markLine是显示了,但是纵坐标的分割区间看起来就像是缺了最上面的一绺。按照预期13330这条标记线应该被包含在12000-15000这个分割区间范围内才对, 这好像跟我们以为的结果不太符合啊,看着十分别扭,这是一个追求完美的前端开发工程师绝对不能容忍的. 然后又仔细研究开发文档, 并没有找到有用的信息, 看来 echarts.yAxis.max 的唯一作用就是强制修改图表能够展示的最大值, 也同时影响坐标刻度的最后一个值. 这不是我们想要的.
来我们先看看原本不添加 markLine 时候的效果⏬,数据应该分布在由几条分割线组成的完整的分割区间内。
来我们再看看我们真正想要的结果⏬,同样的,我们希望包括markLine在内的数据也应该分布在几条分割线组成的完整的分割区间内
所以这是怎么做到的呢? 根据以上需求, 我们只需要得到相邻两条分割线之间的差值 (如上图, 在我的需求中应该是 3000) 和实际需要的分割段数, 然后据此计算出纵坐标最大的坐标轴刻度值, 最后通过 echarts.yAxis.max 返回这个结果即可. 很遗憾, 这两个值均无法直接获得 (也可能是我不知道正确的获取方法, 请知道的大佬告知).
经过我的一番调试, 在 echarts 的源码中找到了纵坐标自动计算坐标轴分割段数相关的算法函数, 下面直接贴源码:
// echarts/lib/scale/helper.js line42 - line74
/**
* @param {Array.<number>} extent Both extent[0] and extent[1] should be valid number.
* Should be extent[0] < extent[1].
* @param {number} splitNumber splitNumber should be >= 1.
* @param {number} [minInterval]
* @param {number} [maxInterval]
* @return {Object} {interval, intervalPrecision, niceTickExtent}
*/
function intervalScaleNiceTicks(extent, splitNumber, minInterval, maxInterval) {
var result = {};
var span = extent[1] - extent[0];
var interval = result.interval = numberUtil.nice(span / splitNumber, true);
if (minInterval != null && interval < minInterval) {
interval = result.interval = minInterval;
}
if (maxInterval != null && interval > maxInterval) {
interval = result.interval = maxInterval;
} // Tow more digital for tick.
var precision = result.intervalPrecision = getIntervalPrecision(interval); // Niced extent inside original extent
var niceTickExtent = result.niceTickExtent = [roundNumber(Math.ceil(extent[0] / interval) * interval, precision), roundNumber(Math.floor(extent[1] / interval) * interval, precision)];
fixExtent(niceTickExtent, extent);
return result;
}
看到在第 14 行(源码58行)调用的 numberUtil.nice() 方法返回纵坐标分割线的刻度间距, 看看 numberUtil.nice() 方法的源码:
// echarts/lib/util/number.js line408 - line458
/**
* find a “nice” number approximately equal to x. Round the number if round = true,
* take ceiling if round = false. The primary observation is that the “nicest”
* numbers in decimal are 1, 2, and 5, and all power-of-ten multiples of these numbers.
*
* See "Nice Numbers for Graph Labels" of Graphic Gems.
*
* @param {number} val Non-negative value.
* @param {boolean} round
* @return {number}
*/
function nice(val, round) {
var exponent = Math.floor(Math.log(val) / Math.LN10);
var exp10 = Math.pow(10, exponent);
var f = val / exp10; // 1 <= f < 10
var nf;
if (round) {
if (f < 1.5) {
nf = 1;
} else if (f < 2.5) {
nf = 2;
} else if (f < 4) {
nf = 3;
} else if (f < 7) {
nf = 5;
} else {
nf = 10;
}
} else {
if (f < 1) {
nf = 1;
} else if (f < 2) {
nf = 2;
} else if (f < 3) {
nf = 3;
} else if (f < 5) {
nf = 5;
} else {
nf = 10;
}
}
val = nf * exp10; // Fix 3 * 0.1 === 0.30000000000000004 issue (see IEEE 754).
// 20 is the uppper bound of toFixed.
return exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val;
}
相关参数源码里已经非常详细地注释了, 此处不做过多解释 (在此赞一下 echarts 的开发团队, 给我们这些菜鸟码农做了一个好榜样👍, 代码注释该写还是要写的)
经过改动, 写了以下函数对 max 值做进一步处理:
/**
* @param span series 中最大值与最小值的差值
* @param splitNumber 坐标轴分割段数
* @param round 折线图中需要传 true
* @returns {number} 处理后 max 的最后结果
*/
function nice(span, splitNumber, round) {
let val = span / splitNumber
var exponent = Math.floor(Math.log(val) / Math.LN10);
var exp10 = Math.pow(10, exponent);
var f = val / exp10; // 1 <= f < 10
var nf;
if (round) {
if (f < 1.5) {
nf = 1;
} else if (f < 2.5) {
nf = 2;
} else if (f < 4) {
nf = 3;
} else if (f < 7) {
nf = 5;
} else {
nf = 10;
}
} else {
if (f < 1) {
nf = 1;
} else if (f < 2) {
nf = 2;
} else if (f < 3) {
nf = 3;
} else if (f < 5) {
nf = 5;
} else {
nf = 10;
}
}
val = nf * exp10; // Fix 3 * 0.1 === 0.30000000000000004 issue (see IEEE 754).
// 20 is the uppper bound of toFixed.
const step = exponent >= -20 ? +val.toFixed(exponent < 0 ? -exponent : 0) : val;
let result
for(let i = splitNumber - 3; i < splitNumber + 3; i++) {
result = step * i
if (result > span) break
}
return result
}
因工作繁忙, 未对以上代码做过优化. 可以根据自己的实际业务需求, 结合 echarts 的 api 对以上代码进行优化, 以适应自己的项目
使用时直接在项目里调一下修改后的方法就可以了:
{
yAxis: {
max: extent => {
const max = extent.max > goal ? extent.max : goal
return nice(max, 5, true)
}
},
series: {
markLine: {
data: [{
yAxis: goal
}]
}
}
}
完整代码就不贴了, 没什么难度。如果还有其他问题,欢迎大家在评论区留言、讨论。