前言:折腾了一个星期,在最后一天中午,都快要放弃了,后来坚持下来,才有下面结果。
这个效果就相当是复合表头,第一行是子级,第二行是父级。
子级是奇数个时,父级label居中很简单,但是,当子级是偶数个的时候,父级就很难居中
如图:
直接把以下源码,复制到这个链接去打开看效果:
链接:https://echarts.apache.org/examples/zh/editor.html?c=bar-simple
查看效果,注意设置实际宽度boxW
const boxW = 547; // 查看效果,一定要根据实际设置宽度,否则父级不会居中
const boxH = 803;
const grid = { left: '10%', right: '10%', bottom: '40%', top: '10%' }
// canvas的宽高
const canvasW = boxW * (1 - parseInt(grid.left) / 100 - parseInt(grid.right) / 100)
const canvasH = boxH * (1 - parseInt(grid.top) / 100 - parseInt(grid.bottom) / 100)
const seriesData = [
{
data: [120, 200, 150, 80, 70, 110, 130, 120, 200, 150, 80, 70, 110, 130],
type: 'bar'
},
{
data: [120, 200, 150, 80, 70, 110, 130, 120, 200, 150, 80, 70, 110, 130],
type: 'bar'
},
{
data: [120, 200, 150, 80, 70, 110, 130, 120, 200, 150, 80, 70, 110, 130],
type: 'line'
}
]
const textStr1 = '第一组123456'
const textStr2 = '第二组第二组第二组第二组1'
const textStr3 = '第三组哈'
const textStr4 = '第四组第四组第四组第四组123456'
const textStr5 = '第五组'
const chartGroups = [
{
grouplabel: textStr1,
xAxis_datas: [textStr1, textStr1]
},
{
grouplabel: textStr2,
xAxis_datas: [textStr2, textStr2, textStr2]
},
{
grouplabel: textStr3,
xAxis_datas: [textStr3, textStr3]
},
{
grouplabel: textStr4,
xAxis_datas: [textStr4, textStr4, textStr4, textStr4, textStr4]
},
{
grouplabel: textStr5,
xAxis_datas: [textStr5, textStr5]
},
]
const xAxisData = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', '日', 'Mon1', 'Tue1', 'Wed1', 'Thu1', 'Fri1', 'Sat1', '日1']
let item2DataArr = [] // x轴的第二行数据
const isShowLabelArr = [] // x轴的第二行 label的显示与隐藏规则
const axisTickArr = [] // 刻度线的显示与隐藏规则
const isExistObj = []
const isExistObj1 = []
const xObj = {}
// 计算x轴的第二行,单元格label的显示与隐藏
chartGroups.forEach(gItem => {
const datas = gItem.xAxis_datas || []
const grouplabel = gItem.grouplabel
const len = datas.length
datas.forEach((o, i) => {
const isEsist = isExistObj1.some(v => v === grouplabel)
// debugger
// 是否显示的设置
if (!isEsist) {
if (len % 2 === 0) { // 当前分组,有偶数个子级
const index = len / 2 - 1
if (index === i) {
// debugger
isExistObj1.push(grouplabel)
isShowLabelArr.push(1) // 1显示,0不显示(标签文字,刻度线)
} else {
isShowLabelArr.push(0) // 1显示,0不显示(标签文字,刻度线)
}
} else { // 当前分组,有奇数个子级
let index = Math.ceil(len / 2) - 1
if (index === i) {
isExistObj1.push(grouplabel)
isShowLabelArr.push(1) // 1显示,0不显示(标签文字,刻度线)
} else {
isShowLabelArr.push(0) // 1显示,0不显示(标签文字,刻度线)
}
}
} else {
isShowLabelArr.push(0) // 1显示,0不显示(标签文字,刻度线)
}
})
})
// 计算x轴的第二行,单元格刻度线的显示与隐藏
chartGroups.forEach(gItem => {
const datas = gItem.xAxis_datas || []
const grouplabel = gItem.grouplabel
datas.forEach((o, i) => {
item2DataArr.push(grouplabel)
const isEsist = isExistObj.some(v => v === grouplabel)
// 是否显示的设置
if (!isEsist) {
isExistObj.push(grouplabel)
axisTickArr.push(1) // 1显示,0不显示(标签文字,刻度线
} else {
axisTickArr.push(0) // 1显示,0不显示(标签文字,刻度线)
}
})
})
// 每一柱子的宽度
const itemW = canvasW / item2DataArr.length
// 整合第二行X轴数据,并过滤重复label
chartGroups.forEach((item, i) => {
const len = item.xAxis_datas.length
// debugger
const centerNum = Math.floor(len / 2) // 当前组的中心
const isOdd = len % 2 === 0
xObj[item.grouplabel] = {
canvasW: boxW,
canvasH: boxH,
itemW,
text: item.grouplabel,
isOdd: isOdd ? '奇数个' : '偶数个',
count: len, // 子级个数(x轴第一行个数)
tdCountW: (len * itemW).toFixed(2) // 合并单元格的总宽度
}
})
// console.log('itemW', itemW)
let richObj = {} // 富文本样式,通过echarts的富文本设置第二行X轴居中
let axisLabelFormat = [] // 富文本显示样式的规则
const spaceW = 4 // 1个空格字符站4px
const perFontW = 12 // 1个字符的宽度12px(根据你的实际情况定义)
let isExistArr = []
let context = null
// 第二行的文字长度区分奇数和偶数,并根据复合单元格宽度,适配文字最大长度
item2DataArr.forEach((k, index) => {
const isTrue = isShowLabelArr[index]
const o = xObj[k]
let txt = o.text
if (isTrue) { // 显示的才处理
const isEsist = isExistArr.some(val1 => val1 === k)
// 计算文字的总宽度
const contextObj = measureTextWidth({ cxt: context, text: k });
if (!context) {
context = contextObj.context
}
o.txtW = contextObj.strWidth; // 文字的总宽度
// debugger
if (o.count % 2 === 0 && !isEsist) { //偶数,需要计算中心位置
let txtAlign = 'left'
let paddingArr = [0, 0, 0, 0]
isExistArr.push(k)
o.halfW = (o.tdCountW - o.txtW) / 2 // 文字在复合单元格中的中心点
o.centerNum = Math.abs(itemW / 2 - o.halfW) // 一个单元格相对文字中心的中心点
o.spaceNum = Math.floor(o.centerNum % spaceW) // 计算把字符从单元格中心移到复合表头中心,需要多少个空字符
const disAllItemW = o.txtW - o.tdCountW
const disItemW = o.txtW - itemW
// debugger
if (disAllItemW > 0) { // 字的长度大于整个复合单元格的宽度
txtAlign = 'center'
paddingArr = [0, 0, 0, itemW]
// debugger
txt = fixTxtMaxWidth({ item: o, context, perFontW }) // 字数长度大于复合单元格宽度(适配复合单元格的宽度,最多能显示多少个字符)
// console.log('\n\n********', txt, 'paddingArr', paddingArr)
} else if (disItemW > 0) { // 字的长度大于1个单元格的宽度
txtAlign = 'center'
txt = k
// debugger
paddingArr = [0, 0, 0, itemW]
// console.log('\n\n----------', o.count, o.text, 'paddingArr', paddingArr)
} else { // 字的长度小于1个单元格的宽度,则需要通过添加空字符来占位
txtAlign = 'left'
txt = fixTxtMinWidth({ item: o, context }) // 子级个数为偶数,且父级字数长度过小,通过给父级label加空格,把label居中显示
// debugger
}
axisLabelFormat.push(`{${index}|${txt}}`)
richObj[index] = {
width: 0.5,
height: 16,
color: '#f00',
padding: paddingArr,
// backgroundColor: '#bbb',
align: txtAlign
}
} else { // 奇数,直接显示中间的即可
// debugger
if (k) {
txt = fixTxtMaxWidth({ item: o, context, perFontW }) // 字数长度大于复合单元格宽度(适配复合单元格的宽度,最多能显示多少个字符)
}
axisLabelFormat.push(`{${index}|${txt}}`)
richObj[index] = {
height: 16
}
}
} else {
axisLabelFormat.push(`{${index}|${txt}}`)
richObj[index] = {
height: 16
}
}
})
console.log(' ')
console.log('itemW', itemW)
console.log('item2DataArr', item2DataArr)
console.log('isShowLabelArr', isShowLabelArr)
console.log('axisTickArr', axisTickArr)
// console.log('canvasW', canvasW)
// console.log('canvasH', canvasH)
console.log('xObj', xObj)
console.log('axisLabelFormat', axisLabelFormat)
console.log('richObj', richObj)
console.log(' ')
// 字数长度大于复合单元格宽度(适配复合单元格的宽度,最多能显示多少个字符)
function fixTxtMaxWidth ({ item, context, perFontW }) {
// console.log('\n\nfixTxtMaxWidth111');
let txt = item.text
let txtLen = item.txtW
const countW = item.tdCountW - perFontW // 超出最大宽度,要裁剪,然后添加省略号
let symbol = ''
// debugger
while (txtLen > countW) {
txt = txt.substring(0, txt.length - 1)
// debugger
const txtObj = measureTextWidth({ cxt: context, text: txt }); // 文字的总宽度
txtLen = txtObj.strWidth
console.log('\nwhile:', txt, txtLen, item.tdCountW)
symbol = '...'
}
txt += symbol
return txt
}
// 通过canvas计算文字宽度
function measureTextWidth ({ cxt, text, fontSize, fontFamily }) {
fontSize = fontSize || 12;
fontFamily = fontFamily || 'Arial';
let context = cxt
if (!context) {
// 创建一个canvas元素
const canvas = document.createElement('canvas');
context = canvas.getContext('2d');
}
// 设置文本样式
context.font = `${fontSize}px ${fontFamily}`;
// 测量文本宽度
const metrics = context.measureText(text);
// console.log(text, metrics.width);
return {
strWidth: metrics.width,
context
}
}
// 子级个数为偶数,且父级字数长度过小,通过给父级label加空格,把label居中显示
function fixTxtMinWidth ({ item, context, dividendNum = 2 }) {
let txt = item.text
let txtLen = item.txtW
const countW = itemW / dividendNum
// debugger
while (txtLen < countW) {
txt = ' ' + txt
const txtObj = measureTextWidth({ cxt: context, text: txt }); // 文字的总宽度
txtLen = txtObj.strWidth.toFixed(2)
// debugger
console.log('fixTxtMinWidth111:', item.txtW, txtLen, itemW, ', tdCountW=', item.tdCountW, txt)
}
return txt
}
option = {
grid,// 组件离容器下侧的距离,值可以是像 20 这样的具体像素值,也可以是像 '20%' 这样相对于容器高宽的百分比
xAxis: [
{
type: 'category',
axisLabel: {
interval: 0,
rotate: 0// 倾斜角度
},
axisTick: {
show: true,
length: 30,
},// 是否显示坐标轴刻度
data: xAxisData
},
// ******************************************************************************************************************************
// 这个是X轴第二行,相当父级
{
type: 'category',
axisLabel: { // 坐标轴文本标签
align: 'center',
formatter (value, index) {
let val1 = axisLabelFormat[index]
return val1 // 返回真,就会显示label
},
interval: function (index, value) {
const val1 = isShowLabelArr[index]
// 根据子级个数动态调整间隔, false则不显示
return val1;
},
rich: richObj
},
position: 'bottom',// 很重要,如果没有这个设置,默认第二个x轴就会在图表的顶部
offset: 30,// X 轴相对于默认位置的偏移,在相同的 position 上有多个 X 轴的时候有用
axisTick: { // 刻度线
show: true,
length: 30,
interval: function (index, value) {
const val1 = axisTickArr[index]
// 根据子级个数动态调整间隔
return val1;
}
},// 是否显示坐标轴刻度
axisLine: { // 是否显示坐标轴轴线
show: true,
onZeroAxisIndex: 2
},
data: item2DataArr
},
// ******************************************************************************************************************************
// 这个设置只是在底部绘制一条线
{
type: 'category',
position: 'bottom',// 很重要,如果没有这个设置,默认第二个x轴就会在图表的顶部
offset: 60,// X 轴相对于默认位置的偏移,在相同的 position 上有多个 X 轴的时候有用
axisLine: { // 是否显示坐标轴轴线
show: true,
onZeroAxisIndex: 2
},
data: []
}
],
yAxis: [
{
name: '人数',
type: 'value'
},
// {
// name: '年龄',
// type: 'value'
// }
],
series: seriesData
};