由于 通过计算位移x轴柱图,因此x轴需要value轴。category类目轴不讨论。
问题
产品希望柱状图显示在坐标轴刻度的右侧,这样就不会在特殊情况下遮挡纵坐标。
横坐标轴为value轴,这样才能更好实现非等间距坐标效果。
非等间距坐标实现,参考Echarts 配置不等间距坐标轴刻度方案记录。
echarts官方并未提供柱状图的偏移配置,series.boundaryGap、series.barGap、series.barCategoryGap均未想到有很好的实现。
方案制定
1.移动横坐标区间
使整体柱状图不压住y轴,首先想到的是使整个x轴的x值减少半个柱状图宽度的值。
就如同中学代数,
设 a = 半个柱状图宽度的值
使 x = x - a 带入原式
但仅使x - a会显示不了最后一个柱状图。原本[0, 50],现在比如[-0.5, 49.5]肯定不行啊,所以在x - a前,xMax要 + 2a(比如[-0.5, 50.5]), 这样就能保证最后一个柱状图显示了。
因此,就需要手动计算并配置xAxis 的max 和 min。这个通过遍历数据源,很快就能得到。
2.x轴上每个值加上a/2
也就是比如原来x= 0 的坐标,现在更换为 x + a
比较
目测是第二种方案更好理解一点。
现在的问题是如何得到a 的值呢。
获取柱状图宽度对应的值
为了便于计算,配置echart barCategoryGap = 0
这样就可以使柱条与柱条间的值,恰好为一个柱状图的宽度。
根据现象推测echarts的算法为,柱条的宽度 = 最靠近(差最小)的两个柱条间的距离
那么代码
/* 最小间距*/
let minGap = Infinity;
for (let i = 0; i < xAxisData.length - 1; i++) {
minGap = Math.min(minGap, xAxisData[i + 1] - xAxisData[i]);
}
那么此时得到minGap 为柱条宽度对应的值。
位移
方案1,移动横坐标区间
接着配置
xAxis.min = xMin - minGap / 2;
xAxis.max = xMax + minGap / 2;
得到
此时,y轴,仍从0开始,需要放在最左侧,可以配置yAxis.axisLine.onZero = false,这样就将整体柱条向右移动了半个柱条距离了。
这种方案的问题是,x坐标的指示不应该在柱条中间了,而是在柱条右侧。
解决的话要么更改markLine 的值减去a,或者重新画个x坐标轴
方案2,每个横坐标值 + a
series.data = dataSource.map(it => [ it[0] + minGap / 2, it[1] ])
该方案鼠标指示器hover上去的时候,显示的值不对,需要减回去。
存在问题
方案1,方案2都存在的问题是,指示器的线在柱图中央。
以下讨论方案1
截至目前,存在柱图过少时,柱条宽度会很宽的问题,如图
因此需要配置柱条的最大宽度。
直接配置series.barMaxWidth 会出现如下情况
此时需要重新计算柱图最大宽度对应的坐标值了
下面由于时间原因,均用方案一作为例子
适配柱条最大宽度
计算x轴长度px
方便计算,事先配置了echarts的grid属性。
const grid = { left: 50, right: 50 };
const echartsEl = document.getElementById('echarts');
const xAxisWidth = echartsEl.offsetWidth - grid.left - grid.right;
计算x轴单位宽度
单位宽度xUnitValue为
到这里还未完,因为实际上我们是要将横坐标xMax = xMax + 2a, x = x - a 的
所以单位宽度的式子应该为
得到x轴单位1对应的px
根据单个柱条的信息,也可单位宽度xUnitValue
有如下式子
那么可建立等式
截至目前已知
- 横坐标长度 xAxisWidth
- 最大最小值 xMax ,xMin
- 最大柱条宽度 barMaxWidth
化简等式可得
这样可求得最大柱条宽度对应的坐标值
之后与上文提到的最小坐标间距minGap做比较即可。
代码
const barMaxWidthValue = (barMaxWidth * (xMax - xMin)) / (xAxisWidth - barMaxWidth);
let barWidthValue = minGap;
if (minGap > barMaxWidthValue) {
barWidthValue = barMaxWidthValue;
}
效果
适配只有一个点的情况
当前算法,若只有一个点,则xMax 与xMin相等,而上面分式可看的有的xMax - xMin作为分母,导致计算异常。
而由图看出,echarts在处理xMin = xMax时,柱图会放在最右侧。
我们希望放在最左侧。
这种情况处理就需要额外判断,如果xMax 与xMin相等,则xMax = 2*xMin
这样便可达成效果:
适配多个系列的情况
当有多个系列时,又出了问题。
推测是echarts在计算横坐标间有剩余空间时,柱条宽度不会压缩。
就比如上图0-2之间有许多空白,那么此时占用的宽度为 2*barWidth + barGap
系列数 * barWidth 的最大值为 minGap
为了便于计算,我把barGap设为0,也就是取消系列间的空白
伪代码
let barMaxWidthValue = (barMaxWidth * (xMax - xMin)) / (xAxisWidth - barMaxWidth * 系列数);
if (xAxisWidth > barMaxWidth * 系列数) {
// 处理系列宽度比x轴还宽的情况
barMaxWidthValue = (barMaxWidth * (xMax - xMin)) / (xAxisWidth - barMaxWidth * 系列数);
}
let barWidthValue = minGap;
if (minGap > barMaxWidthValue * 系列数) {
barWidthValue = barMaxWidthValue * 系列数;
}
效果
关于markLine位置问题
我的想法是再加一个正常的grid坐标轴,覆盖在原坐标轴上方。markLine也从加在这个正常坐标轴中。
便于理解,我拆开给大家看
之后调整下面的空坐标轴位置,与上面坐标重叠就可以了
适配Tooltip指示器
由于覆盖了一个grid在原图上,因此会同时出现两个坐标指示器,和多余的tooltip项。
需要配置原图的xAxis.axisPointer.type = 'none'
tooltip中的内容,考虑配置 tooltip.formatter 隐藏覆盖坐标轴的信息。
全部代码
仅方案1的
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>柱状图显示在坐标轴刻度右侧</title>
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.1.2/echarts.common.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/echarts/4.9.0-rc.1/echarts-en.common.min.js"></script> -->
<body>
<div id="echarts" style="height: 300px;width:100%"></div>
</body>
<script>
let myChart = window.echarts.init(document.getElementById('echarts'));
const barMaxWidth = 20;
let dataSource = [
[0, 10],
[2, 20],
[5, 30],
[10, 40],
[14, 45],
[20, 50],
[30, 57],
[50, 60],
[60, 65],
[90, 70],
[100, 75],
];
const series = new Array(3).fill(null).map((item, i) => {
return {
xAxisIndex: 0,
yAxisIndex: 0,
name: 'Fake Data' + i,
type: 'bar',
barCategoryGap: 0,
barMaxWidth,
barGap: 0,
data: dataSource,
};
});
/* 横坐标*/
const xAxisData = dataSource.map(it => it[0]);
const xMin = Math.min(...xAxisData);
let xMax = Math.max(...xAxisData);
if (xMax === xMin) xMax = 2 * xMin;
const grid = [
{ left: 50, right: 50, top: 20, bottom: 30 },
{ left: 50, right: 50, top: 20, bottom: 30 },
];
const echartsEl = document.getElementById('echarts');
const xAxisWidth = echartsEl.offsetWidth - grid[0].left - grid[0].right;
// const xUnitValue = xAxisWidth / (xMax - xMin);
/* 最小间距*/
let minGap = Infinity;
for (let i = 0; i < xAxisData.length - 1; i++) {
minGap = Math.min(minGap, xAxisData[i + 1] - xAxisData[i]);
}
let barMaxWidthValue = (barMaxWidth * (xMax - xMin)) / (xAxisWidth - barMaxWidth);
if (xAxisWidth > barMaxWidth * series.length) {
// 处理系列宽度比x轴还宽的情况
barMaxWidthValue = (barMaxWidth * (xMax - xMin)) / (xAxisWidth - barMaxWidth * series.length);
}
let barWidthValue = minGap;
if (minGap > barMaxWidthValue * series.length) {
barWidthValue = barMaxWidthValue * series.length;
}
const option = {
tooltip: [{ trigger: 'axis' }],
grid,
xAxis: [
{
gridIndex: 0,
type: 'value',
min: xMin - barWidthValue / 2,
max: xMax + barWidthValue / 2,
axisTick: { show: false }, // 隐藏刻度线
splitLine: { show: false }, // 隐藏分割线
axisLabel: { show: false }, // 隐藏刻度值
axisPointer: {
type: 'none',
// show: false // tooltip会不显示
},
},
{
gridIndex: 1,
type: 'value',
min: xMin,
max: xMax + barWidthValue,
axisTick: { show: false }, // 隐藏刻度线
splitLine: { show: false }, // 隐藏分割线
axisLabel: { show: false }, // 隐藏刻度值
axisPointer: {
triggerTooltip: false,
},
},
],
yAxis: [
{
gridIndex: 0,
type: 'value',
axisLine: {
onZero: false,
},
},
{
gridIndex: 1,
type: 'value',
axisLine: {
onZero: false,
},
axisTick: { show: false }, // 隐藏刻度线
splitLine: { show: false }, // 隐藏分割线
axisLabel: { show: false }, // 隐藏刻度值
},
],
series: [
...series,
{
xAxisIndex: 1,
yAxisIndex: 1,
type: 'bar',
data: dataSource.map(it => [it[0], 0]),
markLine: {
symbol: false, // 取消箭头
silent: true, // 取消鼠标hover事件
label: { position: 'start' }, // 改变label位置
lineStyle: { color: '#ddd', type: 'solid' },
data: dataSource.map(val => {
return {
xAxis: val[0],
};
}),
},
z: 10,
},
],
};
myChart.setOption(option);
</script>
</html>