完整代码可点击vue3项目页面实现echarts图表渐变色的动态配置-星林社区
https://www.jl1mall.com/forum/PostDetail?postId=202410151031000091552查看
一、背景
在开发可配置业务平台时,需要实现让用户对项目内echarts图表的动态配置,让用户脱离代码也能实现简单的图表样式配置。颜色作为图表样式的重要组成部分,其配置方式是项目要解决的重点问题。
二、难点整理
1.可以支持渐变的颜色选择器。
2.渐变色选择器取值结果与echarts图表配置要求的转换
3.图表配置资源存储与更新机制
4.渐变取色器位置配置导致取色器选中的色值发生变化触发事件change频繁被调用,引起最近选取颜色列表更新速率过快和图表刷新过快造成的资源浪费问题等。
三、解决方法
1.渐变取色器
因为开发时间问题,这里直接选用了TDesign的颜色选择器,具体可以参考TDesign官网
<div style="display: inline-block;vertical-align: middle;margin-right: 1rem;" >
<t-color-picker
v-model="lineColor"
defaultValue="#409cff"
:show-primary-color-preview="false"
:recent-colors="recentcolorsList"
@change="changeLineColorThrottle"
/>
</div>
2.渐变色值类型转换
tdesign取色器获取到的渐变色值为形如:linear-gradient(90deg,rgba(0, 0, 0, 1) 0%,rgb(73, 106, 220) 100%) 的css渐变色写法
echarts图表配置渐变色写法为:
"color":{
"colorStops":[
{"offset":0,"color":"#2378f7"},
{"offset":0.5,"color":"#2378f7"},
{"offset":1,"color":"#83bff6"}
],
"x":0,
"y":0,
"x2":1,
"y2":0.5,
"type":"linear",
"global":false
}
因此需要对两种类型进行转换。
1) rgb与hex色值表达转换
function hexToRgb(hex){
let str = hex.replace("#", "");
if (str.length % 3) {
return "hex格式不正确!";
}
//获取截取的字符长度
let count = str.length / 3;
//根据字符串的长度判断是否需要 进行幂次方
let power = 6 / str.length;
let r = parseInt("0x" + str.substring(0 * count, 1 * count)) ** power;
let g = parseInt("0x" + str.substring(1 * count, 2 * count)) ** power;
let b = parseInt("0x" + str.substring(2 * count)) ** power;
return `rgb(${r}, ${g}, ${b})`;
}
function rgbToHex(rgb){
let arr = rgb
.replace("rgb", "")
.replace("(", "")
.replace(")", "")
.split(",");
// 转十六进制
let h = parseInt(arr[0]).toString(16);
let e = parseInt(arr[1]).toString(16);
let x = parseInt(arr[2]).toString(16);
if(h.length<2){
h = '0' + h
}
if(e.length<2){
e = '0' + e
}
if(x.length<2){
x = '0' + x
}
return "#" + h + e + x;
}
2)角度与坐标参数x,x2,y,y2的转化
css中角度是指水平线和渐变线之间的角度,逆时针方向计算。换句话说,0deg 将创建一个从下到上的渐变,90deg 将创建一个从左到右的渐变。【注意很多浏览器(Chrome、Safari、firefox等)的使用了旧的标准,即 0deg 将创建一个从左到右的渐变,90deg 将创建一个从下到上的渐变。换算公式 90 - x = y 其中 x 为标准角度,y为非标准角度。】
linear-gradient() 函数会绘制出一系列与渐变线垂直的彩色线,每条线都匹配与渐变线相交点的颜色。这条渐变线由包含渐变图形的容器的中心点和一个角度来定义的。渐变线上的颜色值是由不同的点来定义,包括起始点、终点,以及两者之间的可选的中间点(中间点可以有多个)。起点是渐变线上代表起始颜色值的点。终点是渐变线上代表最终颜色值的点。这两个点都是由渐变线和从最近的顶点发出的垂直线之间的交叉点定义的。
echarts图表实际上是基于canvas画布展示的,因此需要将起点和终点转换在canvas坐标系中。
css渐变角度与canvas坐标参数转换过程
需要注意的是,渐变线与矩形的交点可能落在水平线上,也可能落在垂直线上,所以需要分情况进行计算,实现代码如下:
function calculateGradientCoordinate(
width,
height,
angle = 180,
) {
if (angle >= 360) angle = angle - 360;
if (angle < 0) angle = angle + 360;
angle = Math.round(angle);
// 当渐变轴垂直于矩形水平边上的两种结果
if (angle === 0) {
return {
x0: Math.round(width / 2),
y0: height,
x1: Math.round(width / 2),
y1: 0,
};
}
if (angle === 180) {
return {
x0: Math.round(width / 2),
y0: 0,
x1: Math.round(width / 2),
y1: height,
};
}
// 当渐变轴垂直于矩形垂直边上的两种结果
if (angle === 90) {
return {
x0: 0,
y0: Math.round(height / 2),
x1: width,
y1: Math.round(height / 2),
};
}
if (angle === 270) {
return {
x0: width,
y0: Math.round(height / 2),
x1: 0,
y1: Math.round(height / 2),
};
}
// 从矩形左下角至右上角的对角线的角度
const alpha = Math.round(
(Math.asin(width / Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2))) *
180) /
Math.PI,
);
// 当渐变轴分别于矩形的两条对角线重合情况下的四种结果
if (angle === alpha) {
return {
x0: 0,
y0: height,
x1: width,
y1: 0,
};
}
if (angle === 180 - alpha) {
return {
x0: 0,
y0: 0,
x1: width,
y1: height,
};
}
if (angle === 180 + alpha) {
return {
x0: width,
y0: 0,
x1: 0,
y1: height,
};
}
if (angle === 360 - alpha) {
return {
x0: width,
y0: height,
x1: 0,
y1: 0,
};
}
// 以矩形的中点为坐标原点,向上为Y轴正方向,向右为X轴正方向建立直角坐标系
let x0 = 0,
y0 = 0,
x1 = 0,
y1 = 0;
// 当渐变轴与矩形的交点落在水平线上
if (
angle < alpha || // 处于第一象限
(angle > 180 - alpha && angle < 180) || // 处于第二象限
(angle > 180 && angle < 180 + alpha) || // 处于第三象限
angle > 360 - alpha // 处于第四象限
) {
// 将角度乘以(PI/180)即可转换为弧度
const radian = (angle * Math.PI) / 180;
// 当在第一或第四象限,y是height / 2,否则y是-height / 2
const y = angle < alpha || angle > 360 - alpha ? height / 2 : -height / 2;
const x = Math.tan(radian) * y;
// 当在第一或第二象限,l是width / 2 - x,否则l是-width / 2 - x
const l =
angle < alpha || (angle > 180 - alpha && angle < 180)
? width / 2 - x
: -width / 2 - x;
const n = Math.pow(Math.sin(radian), 2) * l;
x1 = x + n;
y1 = y + n / Math.tan(radian);
x0 = -x1;
y0 = -y1;
}
// 当渐变轴与矩形的交点落在垂直线上
if (
(angle > alpha && angle < 90) || // 处于第一象限
(angle > 90 && angle < 180 - alpha) || // 处于第二象限
(angle > 180 + alpha && angle < 270) || // 处于第三象限
(angle > 270 && angle < 360 - alpha) // 处于第四象限
) {
// 将角度乘以(PI/180)即可转换为弧度
const radian = ((90 - angle) * Math.PI) / 180;
// 当在第一或第二象限,x是width / 2,否则x是-width / 2
const x =
(angle > alpha && angle < 90) || (angle > 90 && angle < 180 - alpha)
? width / 2
: -width / 2;
const y = Math.tan(radian) * x;
// 当在第一或第四象限,l是height / 2 - y,否则l是-height / 2 - y
const l =
(angle > alpha && angle < 90) || (angle > 270 && angle < 360 - alpha)
? height / 2 - y
: -height / 2 - y;
const n = Math.pow(Math.sin(radian), 2) * l;
x1 = x + n / Math.tan(radian);
y1 = y + n;
x0 = -x1;
y0 = -y1;
}
// 坐标系更改为canvas标准,Y轴向下为正方向
x0 = Math.round(x0 + width / 2);
y0 = Math.round(height / 2 - y0);
x1 = Math.round(x1 + width / 2);
y1 = Math.round(height / 2 - y1);
return { x0, y0, x1, y1 };
}
echarts坐标参数取值范围为0~1,在传参时可以直接将width和height设置为1。
坐标参数转换回角度时,需要注意角度所在象限区域进行分情况讨论。
x=0.5 y=0, x2=0.5, y2=1 从上到下
x=1 y=0.5, x2=0.5, y2=0 从下到上
x=0 y=0.5, x2=1, y2=0.5 从左到右
x=1 y=0.5, x2=0, y2=0.5 从右到左
实现代码如下:
function getDeg(x1,y1,x2,y2){
var x = Math.abs(x1 - x2);
var y = Math.abs(y1 - y2);
var tan = x / y;
var radina = Math.atan(tan); //用反三角函数求弧度
var deg = Math.floor(180 / (Math.PI / radina)) || 0; //将弧度转换成角度
/**
* 根据目标点判断象限(注意是笛卡尔坐标)
* 一: +,+
* 二: -,+
* 三: -,+
* 一: +,-
*/
// * 二、三象限要加 180°
if (x2 < 0 && y2 >= 0) {
deg = 180 + deg;
}
if (x2 < 0 && y2 < 0) {
deg = 180 + deg;
}
// 一、二象限 === 0 就是 180°
if (deg === 0) {
if ((x2 >= 0 && y2 > 0) || (x2 <= 0 && y2 > 0)) {
deg = 180 + deg;
}
}
if ((x2 <= 0 && y2 > 0)) {
deg = 180 + deg;
}
if ((x2 >= 0 && y2 > 0) ) {
deg = - (180 + deg);
}
return deg;
}
此外还需要对色值RGB和HEX进行转换,代码如下:
function hexToRgb(hex){
let str = hex.replace("#", "");
if (str.length % 3) {
return "hex格式不正确!";
}
//获取截取的字符长度
let count = str.length / 3;
//根据字符串的长度判断是否需要 进行幂次方
let power = 6 / str.length;
let r = parseInt("0x" + str.substring(0 * count, 1 * count)) ** power;
let g = parseInt("0x" + str.substring(1 * count, 2 * count)) ** power;
let b = parseInt("0x" + str.substring(2 * count)) ** power;
return `rgb(${r}, ${g}, ${b})`;
}
function rgbToHex(rgb){
let arr = rgb
.replace("rgb", "")
.replace("(", "")
.replace(")", "")
.split(",");
// 转十六进制
let h = parseInt(arr[0]).toString(16);
let e = parseInt(arr[1]).toString(16);
let x = parseInt(arr[2]).toString(16);
if(h.length<2){
h = '0' + h
}
if(e.length<2){
e = '0' + e
}
if(x.length<2){
x = '0' + x
}
return "#" + h + e + x;
}
以上就是css线性渐变格式与echarts线性渐变格式最为关键的转换过程。
3.图表配置资源存储与更新机制
图表的整体配置资源项目通过store进行存储,在页面挂载时进行读取解析。因为onMounted生命周期内,chart配置数据可能还未存储至store中,在此阶段进行初始化可能造成属性值不存在等报错。由于配置组件在配置时已经记录配置值,如果对chartOption进行持续监听会造成冲突,导致取色器异常。因此需要设置flag值判断页面是否为初始化。初始化代码如下:
watch(()=>chartOption,()=>{
//chartOption的反复修改和监听导致的冲突
if(pageUpdateFlag.value){
if('lineStyle' in chartOption.value['series'][0]){
if('type' in chartOption.value['series'][0]['lineStyle'] ){
lineType.value = chartOption.value['series'][0]['lineStyle']['type']
}
if('width' in chartOption.value['series'][0]['lineStyle'] ){
lineWidth.value = chartOption.value['series'][0]['lineStyle']['width']
}
if('color' in chartOption.value['series'][0]['lineStyle'] ){
let color = chartOption.value['series'][0]['lineStyle']['color']
if(typeof color !== 'string' && 'type' in color){
let x = color.x
let y = color.y
let x1 = color.x2
let y1 = color.y2
let deg = 90
deg = getDeg(x-0.5,y-0.5,x1-0.5,y1-0.5)
let colorStr = ''
//渐变颜色选择器里的color要用rgb形式
for(let i =0 ; i < color.colorStops.length;i++){
let curColor = color.colorStops[i].color
if( color.colorStops[i].color[0] == '#'){
curColor = hexToRgb(color.colorStops[i].color[0])
}
colorStr = colorStr + ',' + color.colorStops[i].color + ' ' + color.colorStops[i].offset*100 + '%'
}
let str = "linear-gradient("+deg+'deg'+colorStr+')'
lineColor.value = str
}else{
lineColor.value = color
}
}
}
if('showBackground' in chartOption.value['series'][0]){
isArea.value = true
if('color' in chartOption.value['series'][0]['areaStyle']){
let color = chartOption.value['series'][0]['areaStyle']['color']
if(typeof color !== 'string' && 'type' in color){
let x = color.x
let y = color.y
let x1 = color.x2
let y1 = color.y2
let deg = 90
deg = getDeg(x-0.5,y-0.5,x1-0.5,y1-0.5)
let colorStr = ''
//渐变颜色选择器里的color要用rgb形式
for(let i =0 ; i < color.colorStops.length;i++){
let curColor = color.colorStops[i].color
if( color.colorStops[i].color[0] == '#'){
curColor = hexToRgb(color.colorStops[i].color[0])
}
colorStr = colorStr + ',' + color.colorStops[i].color + ' ' + color.colorStops[i].offset*100 + '%'
}
let str = "linear-gradient("+deg+'deg'+colorStr+')'
areaColor.value = str
}else{
areaColor.value = color
}
}
}else {
isArea.value = false
}
pageUpdateFlag.value = false
}
},{deep:true})
获取到取色值时,需要对chart配置数据更新,并发送图表配置修改信号,让更新数据同步渲染至图表中。
let curColor = value
if(curColor.includes('linear')){
//渐变色,需要解析转换
let colorObj = parseLinearColor(curColor)
chartOption.value['series'][0]['areaStyle']['color'] = deepcopy(colorObj)
}else{
chartOption.value['series'][0]['areaStyle']['color'] = curColor
}
store.changeChartIsChange(true,"option")
4.利用节流函数解决change事件频繁调用导致的一系列冲突问题
在使用TDesign取色器时,通过滑动条调整渐变色偏移量会导致change事件频繁调用,如果直接在change函数中获取value添加到最近使用颜色列表就会造成最近使用列表颜色刷新过快,近似值添加过多的问题。因此需要使用节流函数来避免这一问题。同时change的频繁调用也会导致图表数据更新过快,造成资源浪费等问题。因此本项目使用了节流函数对change的整体事件进行了处理。
节流函数:
import { ref } from 'vue';
export default function useThrottle(fn, delay) {
const canRun = ref(true);
return (...args) => {
if (!canRun.value) return;
canRun.value = false;
setTimeout(() => {
fn(...args);
canRun.value = true;
}, delay);
};
};
取色器change事件:
const changeLineColorThrottle = useThrottle(changeLineColor,400)
function changeLineColor(value,context){
addRecentColor(recentcolorsList,value)
//不能直接value及其值进行任何
//判断取色为纯色还是渐变,注意value为rgb(115, 171, 230)或linear-gradient(90deg,rgb(241, 29, 0) 0%,rgb(73, 106, 220) 100%)
console.log('判断取色为纯色还是渐变',value,lineColor.value,lineColor.value.includes('linear'));
let curColor = value
if(curColor.includes('linear')){
//渐变色,需要解析转换
let colorObj = parseLinearColor(curColor)
chartOption.value['series'][0]['lineStyle']['color'] = deepcopy(colorObj)
}else{
chartOption.value['series'][0]['lineStyle']['color'] = curColor
}
//!!注意:频繁设置状态可能紊乱,导致渐变取色器为黑色
store.changeChartIsChange(true,"option")
}
添加最近使用颜色函数和格式转换函数:
function addRecentColor(recentcolorsList,color){
//添加前应判断数组内是否有重复的色值,有则删除原色值
for(let i =0 ; i < recentcolorsList.value.length; i++){
if(recentcolorsList.value[i] == color){
recentcolorsList.value.splice(i,1)
}
}
recentcolorsList.value.unshift(color)
}
function parseLinearColor(curColor){
//滑动取色时会造成颜色添加频繁
let colorObj = {
"colorStops": [],
"x":0,
"y":0,
"x1":0,
"y1":0,
"type":"linear",
"global":false
}
let deg = curColor.split('linear-gradient(')[1].split('deg')[0]
let colorList = curColor.split('deg')[1].split(',r')
for(let i = 1; i < colorList.length; i++ ){
let arr = colorList[i].split(") ")
let color = 'r'+arr[0]+')'
if(color.includes('rgba')){
color = color.replace('rgba','rgb')
}
color = rgbToHex(color)
let offset = arr[1].split("%")[0]/100
colorObj.colorStops.push({"offset":offset,"color":color})
}
let {x0,y0,x1,y1} = calculateGradientCoordinate(1,1,deg)
colorObj.x = x0
colorObj.x2 = x1
colorObj.y = y0
colorObj.y2 = y1
console.log(deg,x0,y0,x1,y1,"deg转化x0,y0,x1,y1");
return colorObj
}
以上就是实现echarts图表渐变色的动态配置的关键步骤。