前言:我们业务有种图表展示是后端返回N个图表,然后每个图表里面的线不一定,每个线都有各自的单位换算,所有需要多个Y轴,且如果几条线的单位相同,那就只出现一种单位轴线,多个Y轴需要推算每个Y轴的偏移量(offset),一旦legend点击某个线条置灰,那个Y轴就要消失,重新计算Y轴偏移,确保图表不拥挤,美观。
放个效果吧
基于此,我封装了一个方法。代码有点乱糟糟,仅作自己备忘。
新建EchartsUtils.js,如下
import React from 'react';
import * as echarts from 'echarts';
import { EleResize } from '@/utils/esresize';
import dayjs from 'dayjs';
import { clearDeep } from '@/utils/validate';
const color = [
'rgba(79,128,220,0.9)',
'rgba(209,52,186,0.9)',
'rgba(245,200,118,0.9)',
'rgba(255,243,108,0.9)',
'rgba(149,196,101,0.9)',
'rgba(171,205,248,0.9)',
'rgba(10,141,255,0.9)',
'rgba(204,153,255,0.9)',
'rgba(212,108,34,0.9)',
'rgba(153,255,255,0.9)',
'rgba(255,153,204,0.9)',
'rgba(255,204,204,0.9)',
'rgba(153,204,204,0.9)',
'rgba(204,153,153,0.9)',
'rgba(153,255,153,0.9)',
'rgba(149,196,101,0.9)',
'rgba(215,129,19,0.9)',
'rgba(185,109,248,0.9)',
];
/**
* 封装图表方法二 通用监控图表 结合后端
* @author
* charts 所有图表数据 [Array]
如下 :
[
{
"chartName": "",//图表名称
"metricLines": [//指标线集合
{
"chartNodes": [//x,y轴
{
"metricValue": "", //值
"timestamp": 0 //时间
}
],
"metricName": "",//指标名,即线条
"unit": ""//指标单位
}
]
}
]
* chartById 图表所存放容器id标识
* ***/
export const toShowGeneralGraphQueryEchartsOptionUtils = (charts, chartById) => {
const bigNumberTransform = (value) => {
const newValue = ['', '', ''];
let fr = 1000;
let num = 3;
let text1 = '';
let fm = 1;
while (value / fr >= 1) {
fr *= 10;
num += 1;
// console.log('数字', value / fr, 'num:', num)
}
if (num <= 4) {
// 千
newValue[0] = parseInt(value / 1000) + '';
newValue[1] = '千';
} else if (num <= 8) {
// 万
text1 = parseInt(num - 4) / 3 > 1 ? '千万' : '万';
// tslint:disable-next-line:no-shadowed-variable
fm = text1 === '万' ? 10000 : 10000000;
if (value % fm === 0) {
newValue[0] = parseInt(value / fm) + '';
} else {
newValue[0] = parseFloat(value / fm).toFixed(2) + '';
}
newValue[1] = text1;
} else if (num <= 16) {
// 亿
text1 = (num - 8) / 3 > 1 ? '千亿' : '亿';
text1 = (num - 8) / 4 > 1 ? '万亿' : text1;
text1 = (num - 8) / 7 > 1 ? '千万亿' : text1;
// tslint:disable-next-line:no-shadowed-variable
fm = 1;
if (text1 === '亿') {
fm = 100000000;
} else if (text1 === '千亿') {
fm = 100000000000;
} else if (text1 === '万亿') {
fm = 1000000000000;
} else if (text1 === '千万亿') {
fm = 1000000000000000;
}
if (value % fm === 0) {
newValue[0] = parseInt(value / fm) + '';
} else {
newValue[0] = parseFloat(value / fm).toFixed(2) + '';
}
newValue[1] = text1;
}
if (value < 1000) {
newValue[0] = value + '';
newValue[1] = '';
}
return newValue.join('');
};
const toChartValues = (data) => {
let obj = {
max: '-',
min: '-',
avg: '-',
};
if (typeof data === 'object' && data.length > 0 && data?.some((i) => i == 0 || Number(i))) {
let arr = data.map((i) => i && Number(i)).filter((o) => Number.isFinite(o));
obj.max = Math.max(...arr).toFixed(2);
obj.min = Math.min(...arr).toFixed(2);
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += Number(arr[i]);
obj.avg = (sum / arr.length).toFixed(2);
}
}
return obj;
};
//单位换算方法,里面的分别换算就不赘述了
const toShowSeriesValue = (val, unit) => {
let value = val;
if (value === '-') {
return value;
}
if (Number(value) || Number(value) == 0) {
value = Number(value);
} else {
return value;
}
switch (unit) {
case '℃':
return value.toFixed(2) + '℃';
case '%':
return value.toFixed(2) + '%';
case 'ms':
return value.toFixed(2) + 'ms';
case 'kbps':
return bandwidthDisplay(value);
case 'bps':
return bandwidthToSize(value);
case 'Bps':
return bandwidthToSize1(value);
case 'bit':
return throughputDisplay4(value);
case 'Byte':
return throughputDisplay(value);
case 'KB':
return throughputDisplay2(value);
case 'MB':
return throughputDisplay3(value);
case 's':
return formatSeconds(value);
default:
return value + unit;
//return bigNumberTransform(value)
}
};
const deWeight = (data) => {
let arr = [...data];
for (var i = 0; i < arr.length - 1; i++) {
for (var j = i + 1; j < arr.length; j++) {
if (arr[i].unit == arr[j].unit) {
arr.splice(j, 1);
//因为数组长度减小1,所以直接 j++ 会漏掉一个元素,所以要 j--
j--;
}
}
}
return arr;
};
const handleIndex = (arr1, arr2) => {
for (var i = 0; i < arr1.length; i++) {
for (var j = 0; j < arr2.length; j++) {
if (arr1[i].unit == arr2[j].unit) {
arr1[i].yAxisIndex = j;
}
}
}
return arr1;
};
const handleIndexUnit = (seriesName, data) => {
for (var i = 0; i < data.length; i++) {
if (seriesName == data[i].name) {
return data[i].unit;
}
}
};
if (charts.length > 0) {
for (let i = 0; i < charts.length; i++) {
//console.log("图表个数=== ", charts[i]);//图表个数
let totalSeries = [];
let totalYAxis = [];
for (let j = 0; j < charts[i].metricLines.length; j++) {
//data的数量就是图表中线的数量,实现动态加载
//console.log('--------------图表的线条',charts[i].metricLines[j])
let max =
toChartValues(charts[i]?.metricLines[j]?.chartNodes?.map((i) => i.metricValue))?.max ||
'-';
let min =
toChartValues(charts[i].metricLines[j]?.chartNodes?.map((i) => i.metricValue))?.min ||
'-';
let avg =
toChartValues(charts[i].metricLines[j]?.chartNodes?.map((i) => i.metricValue))?.avg ||
'-';
let mySeries = {
unit: charts[i]?.metricLines[j]?.unit,
name:
charts[i].metricLines[j].metricName +
' ' +
' max: ' +
toShowSeriesValue(max, charts[i]?.metricLines[j]?.unit) +
' min: ' +
toShowSeriesValue(min, charts[i]?.metricLines[j]?.unit) +
' avg: ' +
toShowSeriesValue(avg, charts[i]?.metricLines[j]?.unit),
type: 'line',
symbol: 'circle', //实心圆
showSymbol: true, //小点点。每个数据的点是否显示
smooth: true, //平滑
symbolSize: [2, 2],
lineStyle: {
color: color[j % 14],
width: 2,
},
animation: true,
itemStyle: {
color: color[j % 14],
},
yAxisIndex: j,
data:
charts[i]?.metricLines[j]?.chartNodes?.map((o) => {
return [dayjs(o.timestamp).format('YYYY-MM-DD HH:mm:ss'), o.metricValue];
}) || [],
}; //series每条线的数据end
totalSeries.push(mySeries);
let myAxis = {
unit: charts[i]?.metricLines[j]?.unit,
position: j % 2 == 0 ? 'left' : 'right',
offset: j % 2 == 0 ? (j != 0 ? 30 * (j - 1) + 30 : 0) : 30 * (j - 1),
show: true,
nameTextStyle: {
color: color[j % 14],
fontWeight: 'normal',
fontSize: 10,
},
axisLine: {
show: false, //坐标轴是否显示
lineStyle: {
color: color[j % 14],
},
},
axisLabel: {
formatter: function (value, index) {
return toShowSeriesValue(value, charts[i]?.metricLines[j]?.unit);
},
},
axisTick: {
show: false, //坐标轴刻度
},
bz:
charts[i].metricLines[j].metricName +
' ' +
' max: ' +
toShowSeriesValue(max, charts[i]?.metricLines[j]?.unit) +
' min: ' +
toShowSeriesValue(min, charts[i]?.metricLines[j]?.unit) +
' avg: ' +
toShowSeriesValue(avg, charts[i]?.metricLines[j]?.unit),
};
totalYAxis.push(myAxis);
}
// const headerStyle = document.getElementById(chartById+i);
// if(headerStyle) {
// if((totalSeries.length)/2<3){
// headerStyle.style = `height:100%;padding-right:-70px`
// }else{
// headerStyle.style = `height:100%;padding-bottom:-${(totalSeries.length)/2*32}px`
// }
// }
let option = {
title: {
text: charts[i].chartName,
textStyle: {
fontSize: 14,
align: 'left',
color: 'rgba(42,25,91,1)', //title颜色灰色应该就可以
},
x: 'center',
},
grid: {
//bottom:(totalSeries.length)/2<1?(totalSeries.length)*50:(totalSeries.length)/2*66,
bottom:
totalSeries.length / 2 < 1
? totalSeries.length * 50
: totalSeries.length < 4
? (totalSeries.length / 2) * 66
: 60,
right: 40,
left: 40,
//right:(totalSeries.length)/2<1?'8%': `${(totalSeries.length)/2*8}%`,
//left:(totalSeries.length)/2<1?'8%':(totalSeries.length)%2==0 ? `${(totalSeries.length)/2*8}%` :`${(totalSeries.length)/2*9}%`,
containLabel: true,
},
// grid:{
// top:'10%',right:'8%', left:'12%',bottom:70
// },
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
],
tooltip: {
confine: true,
trigger: 'axis',
backgroundColor: '#fff',
textStyle: {
fontSize: 14,
align: 'left',
color: '#808080', //title颜色灰色应该就可以
},
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8, //边框圆角
padding: [10, 10, 10, 10], // 内边距
extraCssText: 'height:auto',
formatter: function (data1) {
//console.log('data1===========',data1);
let detial = data1[0].axisValueLabel + '<br/>';
if (data1.length > 0) {
data1.map((item, index) => {
let itemValue =
item.value[1] == 0 || item.value[1]
? toShowSeriesValue(item.value[1],totalSeries.find(array => array.name === item.seriesName)?.unit)
: '-';
detial =
detial + item.marker + item.seriesName.split(' ')[0] + ' ' + itemValue + '<br/>';
});
}
return detial;
},
},
legend: [
{
icon: 'circle',
bottom: 0,
type: 'scroll',
itemGap: 8,
itemWidth: 10,
data: totalSeries?.map((i) => i.name)?.filter((o, index) => index % 2 == 0),
},
{
icon: 'circle',
bottom: 30,
type: 'scroll',
itemGap: 8,
itemWidth: 10,
data: totalSeries?.map((i) => i.name)?.filter((o, index) => index % 2 != 0),
},
],
//这里legend,grid二次覆盖上面的是支持两种legend样式,不必纠结
legend: {
icon: 'circle',
itemGap: 6,
type: 'scroll',
top: '78%',
left: 'center',
orient: 'vertical',
textStyle: {
fontSize: 10,
},
formatter(val) {
return val;
},
data: totalSeries?.map((i) => i.name),
},
grid: {
right: 30,
left: 30,
top: '16%',
bottom: '30%',
containLabel: true,
},
xAxis: [
{
type: 'category',
nameTextStyle: {
color: 'rgba(147,159,189,1)',
fontSize: 12,
},
axisLine: {
show: false,
lineStyle: {
color: 'rgba(147,159,189,1)',
},
},
axisTick: {
show: false,
},
},
],
yAxis: deWeight(totalYAxis),
series: handleIndex(totalSeries, deWeight(totalYAxis)),
};
if (document.getElementById(chartById + i)) {
let myChart = echarts.init(document.getElementById(chartById + i));
myChart.clear();
myChart.setOption(option, true);
let echartsid = document.getElementById(chartById + i);
let listener = function () {
myChart.resize();
};
//EleResize是让图表随着容器大小变化自由伸缩的方法,下面会贴出来
EleResize.on(echartsid, listener);
myChart.on('legendselectchanged', function (params) {
let select_value = Object.values(params.selected);
let YAxis = totalYAxis.filter((item, index) => {
return select_value[index];
});
totalYAxis = totalYAxis.map((o,index)=>{
let j = YAxis.findIndex(array => array.bz === o.bz)<0?999:YAxis.findIndex(array => array.bz === o.bz);
return {
...o,
position: j % 2 == 0 ? 'left' : 'right',
offset: j % 2 == 0 ? (j != 0 ? 30 * (j - 1) + 30 : 0) : 30 * (j - 1),
}
});
let option = {
title: {
text: charts[i].chartName,
textStyle: {
fontSize: 14,
align: 'left',
color: 'rgba(42,25,91,1)', //title颜色灰色应该就可以
},
x: 'center',
},
grid: {
//bottom:(totalSeries.length)/2<1?(totalSeries.length)*50:(totalSeries.length)/2*66,
bottom:
totalSeries.length / 2 < 1
? totalSeries.length * 50
: totalSeries.length < 4
? (totalSeries.length / 2) * 66
: 60,
right: 40,
left: 40,
//right:(totalSeries.length)/2<1?'8%': `${(totalSeries.length)/2*8}%`,
//left:(totalSeries.length)/2<1?'8%':(totalSeries.length)%2==0 ? `${(totalSeries.length)/2*8}%` :`${(totalSeries.length)/2*9}%`,
containLabel: true,
},
// grid:{
// top:'10%',right:'8%', left:'12%',bottom:70
// },
dataZoom: [
{
type: 'inside',
start: 0,
end: 100,
},
],
tooltip: {
confine: true,
trigger: 'axis',
backgroundColor: '#fff',
textStyle: {
fontSize: 14,
align: 'left',
color: '#808080', //title颜色灰色应该就可以
},
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8, //边框圆角
padding: [10, 10, 10, 10], // 内边距
extraCssText: 'height:auto',
formatter: function (data1) {
//console.log('data1===========',data1);
let detial = data1[0].axisValueLabel + '<br/>';
if (data1.length > 0) {
data1.map((item, index) => {
let itemValue =
item.value[1] == 0 || item.value[1]
? toShowSeriesValue(item.value[1],totalSeries.find(array => array.name === item.seriesName)?.unit)
: '-';
detial =
detial + item.marker + item.seriesName.split(' ')[0] + ' ' + itemValue + '<br/>';
});
}
return detial;
},
},
legend: [
{
icon: 'circle',
bottom: 0,
type: 'scroll',
itemGap: 8,
itemWidth: 10,
data: totalSeries?.map((i) => i.name)?.filter((o, index) => index % 2 == 0),
},
{
icon: 'circle',
bottom: 30,
type: 'scroll',
itemGap: 8,
itemWidth: 10,
data: totalSeries?.map((i) => i.name)?.filter((o, index) => index % 2 != 0),
},
],
legend: {
icon: 'circle',
itemGap: 6,
type: 'scroll',
top: '78%',
left: 'center',
orient: 'vertical',
textStyle: {
fontSize: 10,
},
formatter(val) {
return val;
},
data: totalSeries?.map((i) => i.name),
selected: Object.fromEntries(totalSeries?.map((i,index) => {
let bool = select_value[index] ? true : false;
console.log(bool)
return [
[i.name],bool
]
}))
},
grid: {
right: 30,
left: 30,
top: '16%',
bottom: '30%',
containLabel: true,
},
xAxis: [
{
type: 'category',
nameTextStyle: {
color: 'rgba(147,159,189,1)',
fontSize: 12,
},
axisLine: {
show: false,
lineStyle: {
color: 'rgba(147,159,189,1)',
},
},
axisTick: {
show: false,
},
},
],
yAxis: deWeight(totalYAxis),
series: handleIndex(totalSeries, deWeight(totalYAxis)),
};
myChart.clear();
myChart.setOption(option, true);
});
}
}
}
};
esresize.js文件如下:
var EleResize = {
_handleResize: function (e) {
var ele = e.target || e.srcElement;
var trigger = ele.__resizeTrigger__;
if (trigger) {
var handlers = trigger.__z_resizeListeners;
if (handlers) {
var size = handlers.length;
for (var i = 0; i < size; i++) {
var h = handlers[i];
var handler = h.handler;
var context = h.context;
handler.apply(context, [e]);
}
}
}
},
_removeHandler: function (ele, handler, context) {
var handlers = ele.__z_resizeListeners;
if (handlers) {
var size = handlers.length;
for (var i = 0; i < size; i++) {
var h = handlers[i];
if (h.handler === handler && h.context === context) {
handlers.splice(i, 1);
return;
}
}
}
},
_createResizeTrigger: function (ele) {
var obj = document.createElement('object');
obj.setAttribute(
'style',
'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden;opacity: 0; pointer-events: none; z-index: -1;',
);
obj.onload = EleResize._handleObjectLoad;
obj.type = 'text/html';
ele.appendChild(obj);
obj.data = 'about:blank';
return obj;
},
_handleObjectLoad: function (evt) {
this.contentDocument.defaultView.__resizeTrigger__ = this.__resizeElement__;
this.contentDocument.defaultView.addEventListener('resize', EleResize._handleResize);
},
};
if (document.attachEvent) {
// ie9-10
EleResize.on = function (ele, handler, context) {
var handlers = ele.__z_resizeListeners;
if (!handlers) {
handlers = [];
ele.__z_resizeListeners = handlers;
ele.__resizeTrigger__ = ele;
ele.attachEvent('onresize', EleResize._handleResize);
}
handlers.push({
handler: handler,
context: context,
});
};
EleResize.off = function (ele, handler, context) {
var handlers = ele.__z_resizeListeners;
if (handlers) {
EleResize._removeHandler(ele, handler, context);
if (handlers.length === 0) {
ele.detachEvent('onresize', EleResize._handleResize);
delete ele.__z_resizeListeners;
}
}
};
} else {
EleResize.on = function (ele, handler, context) {
var handlers = ele.__z_resizeListeners;
if (!handlers) {
handlers = [];
ele.__z_resizeListeners = handlers;
if (getComputedStyle(ele, null).position === 'static') {
ele.style.position = 'relative';
}
var obj = EleResize._createResizeTrigger(ele);
ele.__resizeTrigger__ = obj;
obj.__resizeElement__ = ele;
}
handlers.push({
handler: handler,
context: context,
});
};
EleResize.off = function (ele, handler, context) {
var handlers = ele.__z_resizeListeners;
if (handlers) {
EleResize._removeHandler(ele, handler, context);
if (handlers.length === 0) {
var trigger = ele.__resizeTrigger__;
if (trigger) {
trigger.contentDocument.defaultView.removeEventListener(
'resize',
EleResize._handleResize,
);
ele.removeChild(trigger);
delete ele.__resizeTrigger__;
}
delete ele.__z_resizeListeners;
}
}
};
}
export { EleResize };