Open findxc opened 3 years ago
没考虑到负数的情况,还可以再完善一下,做个标记🤓
// 把值拆解为一位数和10的多少次方的乘积
// 比如 120 = 1.2 * Math.pow(10, 2)
const parseDigitAndPow = (num) => {
let pow = 0
while (num < 1) {
num = num * 10
pow = pow - 1
}
while (num >= 10) {
num = num / 10
pow = pow + 1
}
return { digit: num, pow }
}
// 根据一位数拿到对应的 interval
// 第一个大于等于它的就是最合适的 interval
const parseInterval = (digit) => {
const intervals = [1, 2, 3, 5, 10]
for (const interval of intervals) {
if (interval >= digit) {
return interval
}
}
throw Error(`digit: ${digit},没有找到合法 interval `)
}
// 把浮点数运算引起的不准确问题格式化一下(对于太小的数这个方法就不准确了)
const formatFloat = (num) => {
return Number(num.toFixed(8))
}
// 找到大于等于 num 的最佳 interval
const findInterval = (num) => {
if (num <= 0) {
throw Error(`num: ${num},只能为大于 0 的值计算 interval`)
}
const { digit, pow } = parseDigitAndPow(num)
const interval = parseInterval(digit)
const realInterval = interval * Math.pow(10, pow)
return formatFloat(realInterval)
}
function toInt (n) {
if (n > 0) {
return Math.ceil(n) + 1
} else {
return Math.floor(n) - 1
}
}
// 双 y 轴时为了分隔线对齐,计算 interval 和 max
// num1 和 num2 分别是两组数据的最大值
function calculateYMaxAndIntervals (arr) {
let [{ max: max1, min: min1 }, { max: max2, min: min2 }] = arr
max1 = toInt(max1)
min1 = toInt(min1)
max2 = toInt(max2)
min2 = toInt(min2)
let num1 = max1 - min1
let num2 = max2 - min2
// 都为 0 时 返回 echarts 本身的默认值
if (!num1 && !num2) {
return [
{ interval: 0.2, max: 1 },
{ interval: 0.2, max: 1 }
]
}
// 如果有一个为 0 ,那么就设为另外一个的值,也就是直接按有值的去计算
if (!num1) {
num1 = num2
}
if (!num2) {
num2 = num1
}
// 从 份数为 4 5 6 中取最好的情况
const splitNumbers = [4, 5, 6]
// 根据切分的份数计算出对应的 interval
const intervals = splitNumbers.map((n) => {
const num1Avg = num1 / n
const num2Avg = num2 / n
const interval1 = findInterval(num1Avg)
const interval2 = findInterval(num2Avg)
const percent = (num1Avg / interval1 + num2Avg / interval2) / 2
return { interval1, interval2, percent }
})
// 看哪种情况实际的平均值最接近 interval
const percents = intervals.map((x) => x.percent)
const maxPercent = Math.max(...percents)
const index = percents.findIndex((x) => x === maxPercent)
const bestSplitNumber = splitNumbers[index]
const bestInterval1 = intervals[index].interval1
const bestInterval2 = intervals[index].interval2
return [
{
interval: bestInterval1,
max: max1,
min: max1 - formatFloat(bestInterval1 * bestSplitNumber)
},
{
interval: bestInterval2,
max: max2,
min: max2 - formatFloat(bestInterval2 * bestSplitNumber)
}
]
}
console.log(calculateYMaxAndIntervals([{
max: 20.3,
min: -2
}, {
max: 135.6,
min: 2
}]))
小改了一版,记录一下
在两位大佬的基础上小改了一版,记录一下:
// 把值拆解为一位数和10的多少次方的乘积
// 比如 120 = 1.2 * Math.pow(10, 2)
const parseDigitAndPow = (num) => {
let pow = 0
while (num < 1) {
num = num * 10
pow = pow - 1
}
while (num >= 10) {
num = num / 10
pow = pow + 1
}
return { digit: num, pow }
}
// 根据一位数拿到对应的 interval
// 第一个大于等于它的就是最合适的 interval
const parseInterval = (digit) => {
const intervals = [1, 2, 3, 5, 10]
for (let interval of intervals) {
if (interval >= digit) {
return interval
}
}
throw Error(`digit: ${digit},没有找到合法 interval `)
}
// 把浮点数运算引起的不准确问题格式化一下(对于太小的数这个方法就不准确了)
const formatFloat = (num) => {
return Number(num.toFixed(8))
}
// 找到大于等于 num 的最佳 interval
const findInterval = (num) => {
if (num <= 0) {
throw Error(`num: ${num},只能为大于 0 的值计算 interval`)
}
const { digit, pow } = parseDigitAndPow(num)
const interval = parseInterval(digit)
const realInterval = interval * Math.pow(10, pow)
return formatFloat(realInterval)
}
function toInt (n) {
if (n > 0) {
return Math.ceil(n) + 1
} else {
return Math.floor(n) - 1
}
}
// 双 y 轴时为了分隔线对齐,计算 interval 和 max
// num1 和 num2 分别是两组数据的最大值
function calculateYMaxAndIntervals(max1 = 1, max2 = 1, rmin1 = 0, rmin2 = 0) {
let min1 = rmin1
let min2 = rmin2
// 用toInt凑整
max1 = toInt(max1)
max2 = toInt(max2)
min1 = toInt(min1)
min2 = toInt(min2)
let num1 = max1 - min1
let num2 = max2 - min2
// 都为 0 时 返回 echarts 本身的默认值
if (!num1 && !num2) {
return [
{ interval: 0.2, max: 1 },
{ interval: 0.2, max: 1 },
]
}
// 如果有一个为 0 ,那么就设为另外一个的值,也就是直接按有值的去计算
if (!num1) {
num1 = num2
}
if (!num2) {
num2 = num1
}
// 从 份数为 4 5 6 中取最好的情况
const splitNumbers = [4, 5, 6, 7,8,9]
// 根据切分的份数计算出对应的 interval
const intervals = splitNumbers.map((n) => {
const num1Avg = num1 / n
const num2Avg = num2 / n
const interval1 = findInterval(num1Avg)
const interval2 = findInterval(num2Avg)
const percent = (num1Avg / interval1 + num2Avg / interval2) / 2
return { interval1, interval2, percent }
})
// 看哪种情况实际的平均值最接近 interval
const percents = intervals.map((x) => x.percent)
const maxPercent = Math.max(...percents)
const index = percents.findIndex((x) => x === maxPercent)
const bestSplitNumber = splitNumbers[index]
const bestInterval1 = intervals[index].interval1
const bestInterval2 = intervals[index].interval2
// 若两组数据的范围都>=0,则两个坐标系的最小值都取0
if (rmin1 >= 0 && rmin2 >= 0) {
return [
{
interval: bestInterval1,
max: formatFloat(bestInterval1 * bestSplitNumber),
min: 0,
},
{
interval: bestInterval2,
max: formatFloat(bestInterval2 * bestSplitNumber),
min: 0,
},
]
}
// 以下是对于两组数据中至少有一组的最小值<0时的处理,处理目标:
/**
* 1. 两组数据所分的段数相同
* 2. 两组数据的最大值距离0刻度的段数跨度相同
* 3. 两组数据的最小值距离0刻度的段数跨度相同
*
* 上面3点就可以保证两个y轴的刻度splitline能对齐,且0坐标刻度也能对齐
* 以下的计算以上面得出的bestInterval1和bestInterveral2为基准
*/
/**
* 因为上面的max1是向上凑整得到的,不一定是bestInterval1的整数倍,
* i. 因此当max1>0时,我们以bestInterval1的整数倍去递增finalMax1,第一个>=max1的值,
* 就是我们要的finalMax1,finalMax1表示最终确定下来的max1
* ii. 当max1==0时,finalMax1就是0,不用处理
* iii. 当max1<0时,我们以bestInterval1的整数倍去递减finalMax1,最接近 max1 且大于等于max1
* 的值,就是我们要的finalMax1
*/
let finalMax1 = 0
if (max1 > 0) {
while (finalMax1 < max1) {
finalMax1 += bestInterval1
}
} else if (max1 < 0) {
while(finalMax1 > max1) {
finalMax1 -= bestInterval1
}
if (finalMax1 < max1) {finalMax1 += bestInterval1}
}
/**
* min1也是向下凑整得到的,不一定是bestInterval1的整数倍,
* i. 当min1>=0时,finalMin1就是0,不用处理
* ii. 当min1<0时,我们以bestInterval1的整数倍去递减finalMin1,第一个<min1的值,
* 就是我们要的finalMin1
*/
let finalMin1 = 0
if (min1 < 0) {
while(finalMin1 > min1) {
finalMin1 -= bestInterval1
}
}
/**
* 同finalMax1一样处理finalMax2
*/
let finalMax2 = 0
if (max2 > 0) {
while (finalMax2 < max2) {
finalMax2 += bestInterval2
}
} else if (max2 < 0) {
while(finalMax2 > max2) {
finalMax2 -= bestInterval2
}
if (finalMax2 < max2) {finalMax2 += bestInterval2}
}
/**
* 同finalMin1一样处理finalMin2
*/
let finalMin2 = 0
if (min2 < 0) {
while(finalMin2 > min2) {
finalMin2 -= bestInterval2
}
}
// 开始校准
/**
* 经过上面的处理我们得到的finalMax1,finalMin1,finalMax2,finalMin2都分别是自己的bestInterval
* 的整数倍,但是并不能保证finalMin1~finalMax1之间的段数,就和finalMin2~finalMax2之间的段数一样,如果
* 段数不一样,则两组数据的刻度线就不会对齐。
* 而且当finalMin1~finalMax1或者finalMin2~finalMax2或者二者同时出现跨越0刻度线时(也就是数据有正有负),
* 也不能保证finalMin1和finalMin2距离各自0刻度的段数一样,finalMax1和finalMax2距离各自0刻度的段数也不一定一样,这样
* 就不能保证两组数据的0刻度线对齐。
* 因此我们要继续处理finalMin1和finalMin2, finalMax1和finalMax2,目标是满足这两个要求
*/
/**
* i. 若finalMin1和finalMin2都小于0,则比如finalMin1距离0刻度的段数更多,就把finalMin2以bestInterval2的倍数往下递减,
* 直到两者距离0刻度的段数一样
* ii. 若finalMin1或者finalMin2小于0,而另一个finalMin等于0(finalMin最大为0),则把这个finalMin以其bestInterval的倍数往下递减,
* 直到两者距离0刻度的段数一样
*/
if (finalMin1 < 0 && finalMin2 < 0) {
let seg1 = Math.ceil(-finalMin1 / bestInterval1)
let seg2 = Math.ceil(-finalMin2 / bestInterval2)
if (seg1 < seg2) {
const diff = seg2 - seg1
finalMin1 -= bestInterval1 * diff
}
if (seg2 < seg1) {
const diff = seg1 - seg2
finalMin2 -= bestInterval2 * diff
}
} else if (finalMin1 < 0) {
let diff = Math.ceil(-finalMin1 / bestInterval1)
finalMin2 -= diff * bestInterval2
} else if (finalMin2 < 0) {
let diff = Math.ceil(-finalMin2 / bestInterval2)
finalMin1 -= diff * bestInterval1
}
/**
* i. 若finalMax1和finalMax2都>=0,则比如finalMax1距离0刻度的段数更多,就把finalMax2以bestInterval2的倍数往上递增,
* 直到两者距离0刻度的段数一样
* ii. 若其中某个finalMax < 0,则把该finalMax设成其bestInterval的倍数,以使得两者距离0刻度的段数一样
* iii. 若两个finalMax都 < 0,则说明两组数据的范围都在0刻度以下,则不牵扯0刻度线对齐的问题,只要两者划分的段数相同即可,
* 因此比较两者的段数seg1和seg2,假如seg1>seg2,则把finalMin2以bestInterval2的倍数往下递减,以使得两者的段数相等。这里也可以
* 把finalMax2以bestInterval2的倍数往上递增,但是这样有可能触碰到0刻度线,一旦碰到0或者跨越0,就得把finalMax1也往上递增,太麻烦。
* seg1<seg2 也是同理。
*/
if (finalMax1 >= 0 && finalMax2 >= 0) {
let seg1 = Math.ceil(finalMax1 / bestInterval1)
let seg2 = Math.ceil(finalMax2 / bestInterval2)
if (seg1 < seg2) {
const diff = seg2 - seg1
finalMax1 += diff * bestInterval1
}
if (seg2 < seg1) {
const diff = seg1 - seg2
finalMax2 += diff * bestInterval2
}
} else if (finalMax1 >= 0) {
let diff = Math.ceil(finalMax1 / bestInterval1)
finalMax2 = diff * bestInterval2
} else if (finalMax2 >= 0) {
let diff = Math.ceil(finalMax2 / bestInterval2)
finalMax1 = diff * bestInterval1
} else {
// finalMax1 < 0 && finalMax2 < 0
let seg1 = (finalMax1 - finalMin1) / bestInterval1
let seg2 = (finalMax2 - finalMin2) / bestInterval2
if (seg1 > seg2) {
const diff = seg1 - seg2
finalMin2 -= diff * bestInterval2
} else if (seg1 < seg2) {
const diff = seg2 - seg1
finalMin1 -= diff * bestInterval1
}
}
return [
{
interval: bestInterval1,
max: finalMax1,
min: finalMin1
},
{
interval: bestInterval2,
max: finalMax2,
min: finalMin2
},
]
}
export default function calMaxAndMin(data1, data2) {
//分别找出双y轴的最大最小值
// let max1 = Math.max(1, ...data1) || 1;
let max1 = data1.length > 0 ? Math.max(...data1, 1) : 1;
let min1 = data1.length > 0 ? Math.min(...data1, 0) : 0;
// let max2 = Math.max(1, ...data2) || 1;
let max2 = data2.length > 0 ? Math.max(...data2, 1) : 1;
let min2 = data2.length > 0 ? Math.min(...data2, 0) : 0;
const yMaxAndIntervals = calculateYMaxAndIntervals(max1, max2, min1, min2)
return {
y1Max: yMaxAndIntervals[0].max,
y2Max: yMaxAndIntervals[1].max,
y1Min: yMaxAndIntervals[0].min,
y2Min: yMaxAndIntervals[1].min,
y1Interval: yMaxAndIntervals[0].interval,
y2Interval: yMaxAndIntervals[1].interval,
}
}
直接看代码 —> echarts-double-yAxis-splitLine-alignment - CodeSandbox
问题
产品想实现 ECharts Mixed Line and Bar 这种两个 y 轴并且分隔线是对齐的效果。
如果不额外配置的话(注释掉这个例子中 yAxis 中 max 和 interval 的值),就是下面这种效果,两个 y 轴的分隔线是 ECharts 按各自的最优显示去计算的,所以不一定会刚好对齐。
如果我们希望分隔线能对齐的话,就需要手动设置 min, max 和 interval ,使得两个 y 轴的 (max - min) / interval 的值是一样的,也就是两个 y 轴的分割段数是一样的。
一个简单的解决方案
我们先不考虑数据有负数的场景,把 y 轴的 min 设为 0 。
然后把 y 轴分割段数固定为 5 。
这样就只用考虑 interval 的取值了,max 就是 interval * 5 。
interval 最好取整数。我们先计算第一个 y 轴的 interval 。根据第一个 y 轴的真实数据计算得到最大值,比如说是 203.4 ,然后除以 5 ,是 40.68 ,然后向上取整,是 41 ,这就是第一个 y 轴的 interval 的值了。再用同样的方法计算得到第二个 y 轴的 interval 的值。
这样就 ok 了,通过设置 min, max 和 interval ,两个 y 轴都被分为了 5 份,分隔线是对齐的。
但是还存在一个问题,就是你的 interval 计算出来虽然是个整数,但是不够好,你去观察 ECharts 你会发现它的 interval 一般会是 10, 20, 30 等这种比较”整“的数,而不可能是 41, 42, 43 这种,你想想你小时候画坐标轴也不会用这种数字作为间隔。
如何得到一个比较”整“的 interval
你要先知道,什么是比较”整“的 interval 。
通过观察 ECharts 的折线图,我们可以发现,interval 的可能取值有
0.01, 0.02, 0.03, 0.05
,0.1, 0.2, 0.3, 0.5
,1, 2, 3, 5
,10, 20, 30, 50
,发现规律了吗?interval 的取值是
1, 2, 3, 5
的 10 倍、 100 倍或者 0.1 倍、0.01 倍等等。另外一点,如果说我们计算出来的 interval 是 1.2 ,那么我们实际就应该取 2 。如果是 3.6 ,那就应该取 5 。
如果是 12 呢,就是取 20 。如果是 120 呢,就是取 200 。
想想对于任意一个数,如果得到最合适的 interval 值?
如果能把所有可能的 interval 从小到大排列,那第一个大于等于这个数的,就是最合适的 interval 。
但是所有可能的 interval 是无穷的,也就是我们无法定义出这个排好序的数组。
但是我们可以先把数据缩放到 [1, 10) 的范围,然后找到 interval 后再对应缩放回去。
比如 120 是 1.2 100 ,对于 1.2 我们应该取 2 ,然后再用 2 100 得到 200 ,就是 120 对应的最合适 interval 了。
相应的代码如下:
到这里,我们的 interval 计算就 ok 了。但是还有一个小问题,就是我们都是按照 y 轴分为 5 份去计算的,但是其实可能某些场景下其实 y 轴分为 4 份或者 6 份时数据最大值更接近 y 轴最大值,也就是图形会把画布占得更满。
如何让图形尽量占满画布
想要解决这个问题的话,其实也简单,因为我们的期望是数据最大值占 y 轴 max 值的比例尽可能大,那我们就按照 y 轴分为 4、5、6 份来分别计算占 y 轴比例,然后哪个比例最大就用哪个就行了。
写在结尾
在做这个需求的时候,我一开始想的是如何直接实现最后一步的效果,然后发现理不清 ... 就是那种两只手怎么抛接三个球的感觉 ...
然后又梳理了下,把问题拆分,先做最基础功能,然后优化,然后再优化,这样目的更明确,在想解决方案时也可以更专注。
在思考如何计算 interval 的时候,自己是通过去各种测试 ECharts 的表现,找出它的规律,把规律再用代码实现。(当然你也可以直接去看 ECharts 源码?)
最后做完了自己还是挺满足的 hhh 。