Y轴标签重复问题优化

问题背景

如上图所示,项目的图表会出现有明显波动的曲线,但是Y轴的标签是一样的情况,给用户带来了极大的困扰,多次有用户反馈该问题。

问题归因

出现该问题的根因是原始数据精度和Y轴标签精度不匹配。原始数据是后端返回的,可能有多位小数,Y轴标签是原始数据经过一定的转换后得到的,通常会保留到某一位精度,参考下列伪代码:

ts 复制代码
// 原始数据数组
const originValues = [1.1123, 1.093434, 1.1334132, 1.123434, 1.08134];

// 根据原始数据获取对应标签
const getLabel = (originValue) => {
  return originValue.toFixed(1);
};

// 标签数组
const YAxisLabels = originValues.map(originValue => {
  return getLabel(originValue);
});

// [1.1, 1.1, 1.1, 1.1, 1.1]
console.log(YAxisLabels);
                                     

默认情况下,echarts会绘制为如下图所示:

但是天问的图表大部分都开启了YAxis的scale选项,该选项会导致Y轴范围缩小,参考YAxis.scale,便会出现如下图所示情况:

至此,我们便成功复现出该问题。

问题解决

关闭Y轴scale选项

跟产品同学及后端同学沟通,如果不开启scale选项,将有效缓解该问题。后端同学认可该方案,产品同学认可,但仍想保留之前的scale效果,因此最终决定将scale设置为用户自定义配置项,在服务监控JVM页面试水,默认关闭scale,观察用户反馈情况。

2023.09.13上线后至今(2023.11.30),未有用户反馈过JVM页面关闭scale后对他们造成困扰,优化效果良好。

但是,该方案并未解决启用scale情况下的标签重复问题,并且该方案在某些场景下,仍然会出现标签重复的问题,如下列伪代码所示:

ts 复制代码
// 原始数据数组
const originValues = [0.1123, 0.093434, 0.1334132, 0.123434, 0.08134];

// 根据原始数据获取对应标签
const getLabel = (originValue) => {
  return originValue.toFixed(1);
};

// 标签数组
const YAxisLabels = originValues.map(originValue => {
  return getLabel(originValue);
});

// [0.1, 0.1, 0.1, 0.1, 0.1]
console.log(YAxisLabels);

这组数据即使关闭了scale选项,仍然会出现标签重叠问题:

因此,需要实现一套更加优秀的解决方案。

动态Y轴范围算法

重新审视该问题。之所以会产生这个问题,其实是因为Y轴范围太小,导致Y轴标签的差距太小,乃至于小于标签的精度,从而引起了标签重复的问题。那我们是不是只需要将Y轴范围拉大,让相邻两个Y轴标签的差距大于标签的精度,就可以解决这个问题了呢?

动手试一试。首先,定义需要实现的函数getYAxisRange:

ts 复制代码
/**
 *
 * @param min 数据最小值
 * @param max 数据最大值
 * @param unit 数据单位
 * @param decimals 标签精度
 * @returns [Y轴最小值, Y轴最大值],如果最大值或最小值为undefined则表示不特殊需要设置
 */
const getYAxisRange(min: number, max: number, unit?: string, decimals?: number): (number | undefined)[] => {
  // implementation
};

然后在函数的具体实现中,需要获取到最大值和最小值对应的Y轴标签:

ts 复制代码
const minVal = unitFormatter(min, unit, decimals).toString();
const maxVal = unitFormatter(max, unit, decimals).toString();

其中unitFormatter函数就是数据转换函数,参数是原始数据、单位、保留精度,返回值是对应的标签。然后我们需要获取标签中的整数、小数、单位、百分号,用正则表达式进行匹配:

ts 复制代码
// 匹配[123423, 2343.2343, 3432.23434mb, 343.34gb, 243mb, 98.34%]等,匹配格式为[整数, 小数, 单位, 百分号]
const regex = /^(-?\d+)(?:.(\d+))?(\D*)(%)?$/;
const matchMin = regex.exec(minVal);
const matchMax = regex.exec(maxVal);

然后针对一些不用处理的情况,我们直接返回:

ts 复制代码
// 没匹配上
if (!matchMin || !matchMax) {
  return [undefined, undefined];
}
// 单位不同
if (matchMin[3] !== matchMax[3]) {
  return [undefined, undefined];
}

接着我们处理只有整数,没有小数的情况,如果最大值与最小值之间的差值大于等于10,那么是不会出现标签重复问题的,不用特殊处理,因为一个图表一般不会有超过10个标签(如果有,你要考虑一下你的标签是否放得下,会不会重叠,如果非得超过10个,你自己把这个值拉大一点就好,或者可以考虑作为一个参数传入,但我的场景不需要了)。如果小于10,我们需要放大Y轴的范围,这里的处理比较巧妙,对于最小值,先把数值除10后取整再减去0.1,然后再向下取整,是为了保证所得到的值一定比当前标签值小,因为存在这种情况,比如原始数据为0.99,经过转换四舍五入后是1,如果不减去0.1直接向下取整就会得到Y轴最小值为1,那么图表中就不会展示0.99的点,同理你就可以理解最大值的处理方式,另外需要注意,在我的场景中,所有数值均是非负数,因此最小值不能小于0:

ts 复制代码
// 没有小数
if (!matchMin[2] && !matchMax[2]) {
  const minNum = Number(matchMin[1]);
  const maxNum = Number(matchMax[1]);
  // 差值大于等于10返回
  if (maxNum - minNum >= 10) {
    return [undefined, undefined];
  }
  // 否则特殊处理
  const floorMin = Math.max(Math.floor(parseInt((minNum / 10).toString(), 10) - 0.1), 0) * 10 + matchMin[3] + (matchMin[4] ?? '');
  const ceilMax = Math.ceil(parseInt((maxNum / 10).toString(), 10) + 0.1) * 10 + matchMax[3] + (matchMax[4] ?? '');
  const resMin = transformLabel(floorMin, unit);
  const resMax = transformLabel(ceilMax, unit);
  return [resMin, resMax];
}

注意到上述代码中有一个transformLabel函数,这个函数是unitFormatter的反函数,unitFormatter用于将原始数据转换为标签,而transformLabel函数用于将标签转换为原始数据,至于这两个函数的具体实现,是个体力活,不过多赘述,只举一个最简单的例子帮助理解:

ts 复制代码
const unitFormatter = (value: number, unit?: string, decimals = 1) => {
  if (unit === 'SHORT') {
    if (value >= 1000) {
      return (value / 1000).toFixed(decimals);
    }
    return value.toFixed(decimals);
  }
  return value;
}

const transformLabel = (label: string, unit?: string) => {
  if (unit === 'SHORT') {
    const regex = /^(-?\d+(?:.\d+)?)(\D*)$/;
    const match = regex.exec(label);
    if (!match || match.length < 3) {
      return 0;
    }
    if (match[2] === 'k') {
      return Number(match[1]) * k;
    }
  	return Number(match[1]);
  }
  return Number(match[1]);
}

处理完只有整数的情况,接下来我们处理具有小数的情况。首先,鉴于存在小数位数对不齐的情况,我们先填充到相同位数:

ts 复制代码
// 填充位数
const length = Math.max(matchMin[2]?.length ?? 0, matchMax[2]?.length ?? 0);
if (!matchMin[2]) {
  matchMin[2] = '';
}
if (!matchMax[2]) {
  matchMax[2] = '';
}
matchMin[2] = matchMin[2].padEnd(length, '0');
matchMax[2] = matchMax[2].padEnd(length, '0');

接着继续处理,其实和处理整数的思路差不多,无非多了一个逻辑,需要把小数先左移几位,方便Math.floor和Math.ceil处理,然后右移回来(注意判断你的场景先左移的操作是否会导致整型溢出,我的场景不用考虑溢出问题,因此直接暴力乘上去再除就行):

ts 复制代码
 // 如果最大值和最小值相差大于等于精度的10倍,则直接返回
const minNum = Number(matchMin[1] + '.' + matchMin[2]);
const maxNum = Number(matchMax[1] + '.' + matchMax[2]);
if (maxNum - minNum >= Math.pow(10, -(length - 1))) {
  return [undefined, undefined];
}
// 否则特殊处理
const floorMin = Math.max(Math.floor(Number(matchMin[1] + matchMin[2].slice(0, length - 1)) - 0.1) / Math.pow(10, length - 1), 0) + matchMin[3] + (matchMin[4] ?? '');
const ceilMax = Math.ceil(Number(matchMax[1] + matchMax[2].slice(0, length - 1)) + 0.1) / Math.pow(10, length - 1) + matchMax[3] + (matchMax[4] ?? '');
const resMin = transformLabel(floorMin, unit);
const resMax = transformLabel(ceilMax, unit);
return [resMin, resMax];

至此,getYAxisRange函数封装完毕。接下来看看如何使用,首先确认传参,其中unit和decimals都是后端返回的,直接传就好,但是minValue和maxValue就要自己处理一下了,主要有下列几种情况:

  • 单曲线,只需拿到该曲线的最大值、最小值即可。

  • 多曲线,需要拿到所有曲线中的最大值、最小值。

  • 堆叠图,相同时间的数据需要累加,从而获取最大值、最小值。

然后调用getYAxisRange函数,拿到返回值分别赋值给Y轴的最大值和最小值即可,都懂,无需多言。

效果对比

优化前:

优化后:

相关推荐
轻口味35 分钟前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami38 分钟前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
wakangda1 小时前
React Native 集成原生Android功能
javascript·react native·react.js
吃杠碰小鸡1 小时前
lodash常用函数
前端·javascript
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250031 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235952 小时前
web复习(三)
前端
User_undefined2 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app