熟悉echarts的同学应该知道,echarts中的直角系图表当坐标轴为类目型(常为x轴)而且数据又特别多时,默认会隐藏掉一部分x轴文字。效果就类似于这样:
然而,echarts自带的计算有时候是过于苛刻的,即使文字实际上不存在遮挡问题,当文字与文字间的间隔小于一定值时同样会触发隐藏机制,例如上面那张图,如果设置{xAxis:{axisLabel:{interval:0}}}显示出所有的横坐标文字,实际上文字也不会产生遮挡。
另外,有时候即使文字产生遮挡了,我们通过将文字旋转一定的角度也能使文字完全显示,不过遗憾的是,echarts本身并不提供这个功能。先上一张效果图。
还有一种情况,当数据个数较多或文字较长的时候,此时旋转文字已经于事无补,这里我们的解决方案是将图表进行转置,即柱形图变为条形图,同时对左边文字进行预换行处理,而后根据UI提供的柱子宽度和间隔对承载echarts的DOM元素做增高处理。效果是这样的。
(下面的效果图在DOM元素外又包了一层用于实现展开收起动画,并且默认显示所有label)
待展开
已展开(缩放可能有些模糊)
首先声明,本文并不是一篇多实用的教程,因为功能比较复杂,设计的知识点比较多,而且代码和页面结构具有一定的耦合性,所以看了本文的人不一定就写的出本文的效果,仅供大家参考,也是为我自己做一些记录,万一以后有类似的需求就可以快速部署。不懂原理只会复制粘贴的人可以止步了,复制过去也会有很多错误。
另外,代码中所涉及的页面结构如下:
//伪代码
// var chartDOM = globalTools.data.echartsDOM = $('#chart')[0];
//var chart = globalTools.data.echartsContainer = echarts.init(chartDOM);
注:本文所有的函数一般有附带有其还原操作或函数,例如showAllLabel(显示所有label)和hideAllLabel(隐藏所有label)两个函数相对应。
另外,对坐标轴文字进行换行的操作用到了我之前写的另外一篇文章里的函数,dc.getWrapString();//文章链接如下:
JS文本换行算法-模拟计算文字换行位置-基于DOM元素自发换行行为和字符分割原理-支持实体编码、不支持标签嵌套和富文本
并在xAxis的axisLabel.fomatter进行了如下设置:
var xAxis = {
axisLabel: {
color: "#999999",
textStyle: {
fontSize: 12
},
formatter: function(value,index){
var fontSize;
try{
fontSize = globalTools.data.echartsContainer.getOption().xAxis[0].axisLabel.fontSize;
}catch(e){fontSize=12}
width = '135px';
if(globalTools.data.isReverse) width = '219px';
var result = dc.getWrapString(value,{
fontSize: fontSize+'px',
width: width
});
if(typeof index === 'number'){
if(index==0){
d.data.xAxisLabel = []
}
if(d.data.xAxisLabel&&d.data.xAxisLabel.indexOf(result)<0) d.data.xAxisLabel.push(result);
}
return result;
},
showMaxLabel: true,
showMinLabel: true
}
}
效果是这样的:
以下对xAxis的计算全部基于上述设置(即文本预换行和强制显示首尾项)
//注,代码中的短标识符d等同于长标识符globalTools,而dc则是另外一个标识符。两者都是一个对象,对象中定义了某些函数或常变量。
先解释一下我的思路:
首先,判断x轴文字是否产生遮挡问题并进行适应的函数名称为rotateXAxisIfNeed,取名含义就是字面上的意思。要完成这个算法要求用到一些辅助函数,其中有一些我已经提到过,例如计算一行字符占用宽度的js函数(d.getStringWidth()或globalTools.getStringWidth())。链接:
js小工具-模拟计算一行文本的宽度
此函数的思路如下:
1.先计算出当前图表可用的宽度大小,即chartDOM的宽度-grid占用-y轴占用宽度 之后剩下的宽度就是实际绘图的可用宽度。
2.区别对待x轴为类目轴(category)和数值轴(value)的情况,主要处理类目轴。
3.假设所有坐标文字都有进行显示,取坐标文字中占用宽度最大的一项的宽度作为平均宽度,估算坐标文字的宽度累加值是否超过绘图可用宽度。若没超过则直接显示所有坐标文字,结束函数,否则继续判断。
4.假定旋转角度为45度,先记录正弦值和余弦值用来等下判断两端溢出。然后取坐标文字中行数最多的一项的行数乘以字体大小作为旋转后的占用宽度,判断宽度累加值是否超过绘图可用宽度,若没超过,则对文字进行旋转,结束函数,否则继续。
5.当旋转后还是无法显示所有坐标文字时,则进行图表转置,转置的代码定义在d.chartReverse中。
图表的转置其实是非常简单的,但是这里由于项目需要的关系,我在转置中还调用了一些其他函数,例如d.chartSizeFix()用来对图表进行自动增高或还原等。
同时代码还结合在另外两篇文章里提到的计算图例高度和标题高度来对图片进行适应性设置(d.getTitleHeight和d.getLegendHeight)。链接:
echarts标题高度计算-暂不支持富文本-类似于图例
echarts图例高度计算-横向图例-legend组件高度模拟计算-冷知识
注意:在下列的函数计算中,调用的echart实例API的setOption()操作实测会有300ms的计算延时,所以为了提高代码效率,代码中多处地方使用了setOption(option,false,true);//即setOption的第三个参数传入true代表lazuUpdate,详见echarts官方文档。
我估计下面的代码应该很多人会有疑问的,仅供参考,也给我自己做一下记录.
var d = globalTools = {
//根据图例和标题的高度等自动设置echarts的grid //0.3s或2.1s
fitGird: function(noReverse){
//console.log('fit',norotate);
var chart = globalTools.data.echartsContainer;
var legendHeight = globalTools.getLegendHeight();
var titleHeight = globalTools.getTitleHeight();
var yAxisOverflow = 30;//预留给y轴的name子组件
try{
var legend = chart.getOption().legend[0];
var fontSize = legend.textStyle.fontSize ||12;
yAxisOverflow = Math.ceil(fontSize*2.5);
}catch(e){}
var legendTop = 5;//legend顶部预留5px
chart.setOption({
grid:{
top: Math.ceil(yAxisOverflow+legendHeight+titleHeight)
},
legend:{
top: Math.ceil(legendTop+titleHeight)
}
},false,true);
d.rotateXAxisIfNeed(noReverse);
},
//计算echarts内部图例占用高度(不考虑富文本的情况) //5ms
getLegendHeight: function(index){
var height =0;
var charDOM = globalTools.data.echartsDOM;
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var legends = option.legend;
if(!legends||legends.length<=0) return 0;
index = parseInt(index);
if(isNaN(index)||index<0||index>=legend.length) index = 0;
var legend = legends[index];
if(!legend||!legend.show||!legend.data||legend.data.length<=0) return 0;
//主算法,将legend中的设置渲染为DOM元素,用dom元素的宽高来模拟legend组件在echarts内部的高度
var icongap = 5;//legend图形左右两边各有5个px的间隔
var left = d.formatNum(legend.left),right = d.formatNum(legend.right);
//计算legend组件的可用宽度
var chartWidth = legend.width||$(charDOM).width()-left-right;
//legend的padding
var padding = legend.padding || 0;
if($.isArray(padding)) padding = padding.join('px ')+'px';
else padding+='px';
//每个legend item之间的间隙(包括水平和垂直)
var itemGap = legend.itemGap;
//创建一个不可见的模拟盒子
var $legendbox = $('').css({
width: (chartWidth+itemGap) +'px',
padding: padding,
'line-height': '1',
'box-sizing': 'border-box',
overflow: 'hidden',
'position': 'absolute',
'z-index': '-1',
'opacity': '0',
'filter': 'alpha(opacity=0)',
'-ms-filter': 'alpha(opacity=0)'
}).appendTo('body');
//模拟绘制单个legend item
var itemHeight = d.formatNum(legend.itemHeight),itemWidth = d.formatNum(legend.itemWidth);
if(itemHeight%2!=0) itemHeight++;
if(itemWidth%2!=0) itemWidth++;
var fontSize = legend.textStyle.fontSize || 12;
var fontWeight = legend.textStyle.fontWeight || 'normal';
$.each(legend.data,function(i,name){
var $icon = $('').css({
display: 'inline-block',
padding: '0 '+icongap+'px',
'box-sizing': 'content-box',
'width': itemWidth+'px',
'height': itemHeight+'px'
});
var $item = $('').css({
'display': 'inline-block',
'float': 'left',
'margin-right': itemGap+'px',
'margin-bottom': itemGap+'px',
'font-size': fontSize+'px',
'font-weight': fontWeight
}).append($icon).append(name).appendTo($legendbox);
});
//得到模拟高度
height = $legendbox.innerHeight()-itemGap;
//善后工作
$legendbox.remove();
return height;
},
//计算标题占用高度(不考虑富文本的情况) //1ms
getTitleHeight: function(index){
var height = 0;
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var titles = option.title;
if(!titles||titles.length<=0) return 0;
index = parseInt(index);
if(isNaN(index)||index<0||index>=title.length) index=0;
var title = titles[index];
if(!title||!title.show) return 0;
//标题字号
var fontSize = title.textStyle.fontSize || 18;
var fontWeight = title.textStyle.fontWeight || 'normal';
//标题文字
var text = title.text;
//title的padding
var padding = title.padding || 0;
if($.isArray(padding)) padding = padding.join('px ')+'px';
else padding+='px';
var $div = $('').html(text).css({
'white-space': 'pre',
'box-sizing': 'content-box',
'font-size': fontSize+'px',
'font-weight': fontWeight,
'line-height': '1',
padding: padding,
'position': 'absolute',
'z-index': '-1',
'opacity': '0',
'filter': 'alpha(opacity=0)',
'-ms-filter': 'alpha(opacity=0)'
}).appendTo('body');
height = $div.innerHeight();
$div.remove();
return height;
},
//图表转置 //执行时间约6ms或1.5s()
chartReverse: function(norotate){
globalTools.resetAxisLabel(true);
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var series = option.series;
var xAxis = option.xAxis;
var yAxis = option.yAxis;
//交换xAxis和yAxis
option.yAxis = xAxis;
option.xAxis = yAxis;
//交换后对position进行校正
$.each(option.xAxis,function(i,xa){
if(xa.position=='top'||xa.position=='right') xa.position = 'top';
else xa.position = 'bottom';
});
$.each(option.yAxis,function(i,ya){
if(ya.position=='top'||ya.position=='right') xa.position = 'right';
else ya.position = 'left';
});
//交换每个系列的xAxisIndex和yAxisIndex。并且当数组为数组时,则数组前两位代表x和y,也需要交换
$.each(series,function(i,sr){
var xi = parseInt(sr.xAxisIndex);
var yi = parseInt(sr.yAxisIndex);
if(!isNaN(xi)){
sr.yAxisIndex = xi;
}else{
delete sr.yAxisIndex;
}
if(!isNaN(yi)){
sr.xAxisIndex = yi;
}else{
delete sr.xAxisIndex;
}
$.each(sr.data,function(i,data){
if($.isArray(data)){
var x = data[0];
var y = data[1];
data[0] = y;
data[1] = x;
}
});
});
chart.setOption(option,true);
globalTools.chartSizeFix(norotate);
},
//条形图尺寸自适应 //执行时间约0.6s或0.3s或6ms
chartSizeFix: function(norotate){
var chartDOM = globalTools.data.echartsDOM;
var $chartcontainer = $('.chart-container');
var $chart = $(chartDOM);
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var yAxis = option.yAxis[0];
var barWidth = 24;
var gap = 10;
var grid = option.grid[0];
var top = globalTools.formatNum(grid.top),bottom = globalTools.formatNum(grid.bottom);
var height = $chart.height();
//var fontSize = yAxis.axisLabel.fontSize || 12;
//var yAxisOverflow = Math.ceil(fontSize*2.5);
//尺寸自适应暂时只针对于条形图
if(!norotate)
globalTools.rotateXAxisIfNeed(false,true);
if(yAxis.type!='value'){
d.data.isReverse = true;
var sum = 0;
$.each(option.series,function(i,sr){
sum += sr.data.length || 0;
});
d.showAllAxisLabel(true);
d.showAllLabel(true);
var tmp;
if(height<(tmp=(barWidth+gap)*sum + top + bottom)){
if(!$chart.attr('data-initheight')) $chart.attr('data-initheight',height)
$chart.css({
height: tmp+'px'
});
chart.resize();
//判断高度是否溢出
$chartcontainer.addClass('vertical');
if($chart.height()<=$chartcontainer.height()){
$chartcontainer.removeClass('vertical');
}
}
}else{
d.data.isReverse = false;
$chartcontainer.removeClass('vertical');
$chart.css({
height: $chart.attr('data-initheight')+'px'
});
d.hideAllLabel(true);
chart.resize();
}
},
//获取所需y轴的所有axisLabel //0.3s
getAxisLabel: function(xa,Axis,index,name){
var chart = globalTools.data.echartsContainer;
var arr = [],axisLabel = [];
if(name=='yAxis'&&Axis.length==1&&d.data.yAxisLabelData&&d.data.yAxisLabelData.length>0) return d.data.yAxisLabelData;
for(var i=0;i0){
var label = axisLabel.sort(function(a,b){
return (b+"").length-(a+"").length;
})[0];
width += dc.getStringWidth(label,{"font-size":fontSize+'px',"font-weight":fontWeight});
}else{//
console.warn('no axisLabel found at option.yAxis['+yas[i].index+'].');
}
width += yas[i].offset + yas[i].axisLabel.margin;
}
//第3步还原图表设置
//chart.setOption(option);
return width;
},
//获取x轴可见的axisLabel // 0.3s 或 6ms(取决于是否有缓存)
getxAxisLabel: function(index){
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var xAxis = option.xAxis;
index = parseInt(index);
if(isNaN(index)||index<0||index>=xAxis.length) index = 0;
var xa = xAxis[index];
var arr;
if(xAxis.length==1){
arr = d.data.xAxisLabel;//如果有缓存则取缓存,如果图表数据是动态的则需要去掉本段代码
}
if(!arr){
arr = d.getAxisLabel(xAxis[index],xAxis,index,'xAxis');
d.data.xAxisLabel = arr;
}
return arr;
},
//重置x轴 //需要转置时,执行时间约为0.3s 或 6ms(取决于lazy)
resetAxisLabel: function(lazy){
lazy === true ? true : false;
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var xAxis = option.xAxis[0],yAxis = option.yAxis[0];
if(xAxis.type=='category'){
xAxis.axisLabel.interval = 'auto';
xAxis.axisLabel.rotate = 0;
}
if(yAxis.type=='category'){
yAxis.axisLabel.interval = 'auto';
yAxis.axisLabel.rotate = 0;
}
chart.setOption(option,false,lazy);
},
//显示所有坐标文字 //0.3s //6ms
showAllAxisLabel: function(lazy){
lazy === true ? true : false;
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
var xAxis = option.xAxis[0],yAxis = option.yAxis[0];
if(xAxis.type=='category'){
xAxis.axisLabel.interval = 0;
}
if(yAxis.type=='category'){
yAxis.axisLabel.interval = 0;
}
chart.setOption(option,false,lazy);
},
//显示所有数据数值 // 0.3s //6ms
showAllLabel: function(lazy){
lazy === true ? true : false;
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
$.each(option.series,function(i,sr){
sr.label = {
color: '#333',
show: true,
position: 'right',
distance: 8,
fontSize: 13,
fontWeight: 'bold',
formatter: function(param){
var index = 1;
if(globalTools.data.isReverse) index = 0;
var data = param.data;
var value = $.isArray(data) ? data[index]: data;
value = parseFloat(value);
if(isNaN(value)) value = '';
//当数值非小数时至多保留2位小数
if(Math.abs(value) > 0) parseFloat(value.toFixed(2))
return value;
}
}
});
chart.setOption(option,false,lazy);
},
//隐藏所有数据数值 //0.3 //6ms
hideAllLabel: function(lazy){
lazy === true ? true : false;
var chart = globalTools.data.echartsContainer;
var option = chart.getOption();
$.each(option.series,function(i,sr){
sr.label = {
show: false
}
});
chart.setOption(option,false,lazy);
},
//直角系类目轴刻度标签矫正函数,需要时通过控制文字旋转来尽量多的展示文字(不考虑富文本的情况)
//1.8s
rotateXAxisIfNeed: function(noReverse,lazy){//当图表宽度变化时需要重新计算
//console.log('rotate',noReverse);
var chart = globalTools.data.echartsContainer;
var chartDOM = globalTools.data.echartsDOM;
var $chart = $(chartDOM);
var formatNum = globalTools.formatNum;
//获取图表现行option的副本
var rotate = 45;//45deg
var option = chart.getOption();
var xAxis = option.xAxis[0];
var fontSize = xAxis.axisLabel.fontSize || 12;
var fontWeight = xAxis.axisLabel.fontWeight || 'normal';//fontWeight也会影响文字大小
var styles = {
"font-size": fontSize+'px',
"font-weight": fontWeight,
"white-space": 'pre'
};
var grid = option.grid[0];
var gleft = formatNum(grid.left),gright = formatNum(grid.right);
//x轴数据的默认间隔
var gap = 10;//10px
var temp;
var xAxisLabel = globalTools.getxAxisLabel();
var lefty = d.getYAxisWidth('left'),righty = d.getYAxisWidth('right');
try{
var xformatter = xAxis.axisLabel.formatter;
//判断x轴是否为有效的类目轴
if(xAxis.show && xAxis.type=='category'&&xAxis.data.length>0){
var xData = xAxis.data.map(function(v){if(typeof xformatter === 'function') v=xformatter(v);return v;});
//设置grid的属性
if(!grid.containLabel) grid.containLabel = true;
//获取图表宽度
var gridWidth = $chart.width() -globalTools.getYAxisWidth() - gleft - gright;
//x轴数据的间隔是相等的,所以计算遮挡时要取x轴数据中最长的那个
var mxa = xData.sort(function(a,b){
var alen = dc.getStringWidth(a,styles),blen=dc.getStringWidth(b,styles);
return blen-alen;
})[0];
var longestx = xData[0];
//计算需要的宽度
var xDataWidth = (dc.getStringWidth(mxa,styles)+gap)*xData.length-gap;
if(gridWidth0&&(temp=2+ngleft-(gleft+lefty+(gridWidth/xData.length/2)))>0){
gridWidth-=temp;
grid.left = gleft+temp;
}
if(sin<0&&(temp=2+ngright-(gright+righty+(gridWidth/xData.length/2)))>0){
gridWidth-=temp;
grid.right = gright+temp;
}
var line = 1;
xData.sort(function(a,b){
var alen=0,blen=0;
try{
alen = a.split('\n').length;
blen = b.split('\n').length;
}catch(e){}
return blen-alen;
});
line = xData[0].split('\n').length;
xDataWidth = ngleft+(fontSize*line+gap)*xData.length - gap;
if(gridWidth0){
grid.left = gleft+temp;
}
if((temp=2+dc.getStringWidth(xAxisLabel[xAxisLabel.length-1],styles)/2-(gright+righty+(gridWidth/xData.length/2)))>0){
grid.right = gright+temp;
}
}else{
//此函数大概话费1~2s
globalTools.chartReverse();
return;
}
}else{//走到这里说明旋转之后放得下
xAxis.axisLabel.interval = 0;
xAxis.axisLabel.rotate = rotate;
var margin = 8+formatNum(Math.ceil(sin*fontSize*line/2));
xAxis.axisLabel.margin = margin;
//图表增高
$chart.css({
'height': (margin-8+$chart.height()-fontSize+Math.ceil(sin*dc.getStringWidth(longestx,styles)))+'px'
});
chart.resize();
}
}else{//走到这里说明,即使把横坐标文字全部显示也不会造成遮挡,设置interval为0确保横坐标全部显示
xAxis.axisLabel.interval = 0;
xAxis.axisLabel.rotate = 0;
//确保两端的axisLabel不会超出宽度 abstract
if((temp=2+dc.getStringWidth(xAxisLabel[0],styles)/2-(gleft+lefty+(gridWidth/xData.length/2)))>0){
grid.left = gleft+temp;
}
if((temp=2+dc.getStringWidth(xAxisLabel[xAxisLabel.length-1],styles)/2-(gright+righty+(gridWidth/xData.length/2)))>0){
grid.right = gright+temp;
}
}
chart.setOption(option,true,lazy);
}else if(xAxis.type=='value'){//数值轴单独处理
if((temp=2+dc.getStringWidth(xAxisLabel[0],styles)/2-(gleft+lefty))>0){
grid.left = gleft+temp;
}
if((temp=2+dc.getStringWidth(xAxisLabel[xAxisLabel.length-1],styles)/2-(gright+righty))>0){
grid.right = gright+temp;
}
chart.setOption({
grid:{
left: grid.left,
right: grid.right
}
},false,lazy);
}
}catch(e){console.warn('Exception while correcting xAxis label',e);}
}
};
//代码入口函数为d.fitGrid();当只需要设置x轴时也可以直接调用d.rotateXAxisIfNeed();
本文写于20190827,仓促总结,有待完善。