目录
需求背景
柱状图/条形图用于大数据表的展示。
屏幕视图容器长度有限。
单个分组(或者理解为柱子群)需要展示庞大的数据量。
技术缺陷
在Echarts中,我们使用datazoom进行滑动。
参考我的推文:
(43条消息) 【柱状图】全网唯一 图表组件与仪表盘的全屏滚动设计思路_AI3D_WebEngineer的博客-CSDN博客https://blog.csdn.net/weixin_42274805/article/details/125514048?spm=1001.2014.3001.5502
(43条消息) 【Echarts】解决[柱状图设置barMinWidth导致的柱体重叠]与解决[柱状图设置barMinHeight导致数值0也有高度]_AI3D_WebEngineer的博客-CSDN博客_echarts柱状图的高度https://blog.csdn.net/weixin_42274805/article/details/125265161
return userOPT.dataZoom.map(item => ({
id: item.id,
type: item.type,
xAxisIndex: item.xAxisIndex,
filterMode: item.filterMode,
zoomLock: false,
moveOnMouseWheel: true,
moveOnMouseMove: false,
preventDefaultMouseMove: true,
zoomOnMouseWheel: false,
startValue: 0,
endValue,
}));
可以看到。datazoom是由startValue和endValue来决定滑动的初屏和滑动区间。(startValue和endValue可以看做柱群的下标)
但是,
当一个柱子群需要展示的柱子数量过多(一屏无法展示完)还需要整数n屏或者比n屏还要多出半屏或部分屏。
此时
startValue - endValue = 0
顾名思义,起始和终止的下标都是相等(只指向一个柱子群)
此时滑动被冻结,无法进行正常滑动
解决思路遇到的问题:
因为startValue 和 endValue是指向下标,所以当他们相等时,一定无法被滑动。在不修改源码的情况下,我们可以这么解决:
通过设置start和end,取代startValue和endValue,从而实现滑动。
start和end是百分比,取值是0-100
当start为0时,无法上滑。
当end为100时,无法下滑。
此时未设置barMinWidth的柱状图可以正常滑动。但是数据堆在一起根本看不清。
那么我们设置一下barMinWidth
barMaxWidth: 300,
barMinHeight: 10,
barMinWidth: 10,
barCategoryGap: 20,
barGap: '10%',
![](https://i-blog.csdnimg.cn/blog_migrate/b6449442b6672f43f588c6279fb88278.png)
![](https://i-blog.csdnimg.cn/blog_migrate/171a1e80fe0eacdf091c950cbb8b4cbb.png)
可以看到设置了minWidth后,使用start和end滑动会错位,造成柱子重叠。
柱状图,出于对视图大小的兼容,最好还是自己计算最大容纳柱子数,然后通过series进行数据控制。
解决方案之前的技术准备:
start和value,文档介绍的很简单。就是数据范围的百分比,那么真的有那么简单吗?
当我把slider打开时,start和end的概念是不是就呼之欲出了?
XAxis的data是: ['华东', '华南', '华中', '西南', '东北', '华北', '西北'],
那我们是不是可以这么算?
start和end与XAxis的data对应
[0 -1/7 *100)
[1/7*100 -2/7*100)
[2/7*100-3/7*100)
[3/7*100-4/7*100)
[4/7*100-5/7*100)
[5/7*100-6/7*100)
[6/7*100-100*100)
[0 -1/7 *100)大概是0 - 14.2
填进去!
错误!出现两个柱子群
笔者填值猜测,大概得到如下对应数据:
start和end与XAxis的data对应
[0 -8)
[9-24)
[25-41)
[42-58)
[59-74)
[75-91)
[92-100)
这完全是一个不规则数组!!!
可以看到差距既有8又有15又有16,那么会不会!
start:
0 + n
(0 + n) + 2n
(0+ n + 2n) + 2n
.....
开头结尾都是+1个步进值
中间是2个步进值(步进值是 100 / n个 也就是7)
export function createEchartsDataZoomModel(n) {
if (!n) {
return [];
}
if (n === 1) {
return [0];
}
// 首尾为n步进,中间是2n步进
const stepNumber = 100 / ((n - 2) * 2 + 2);
const dataZoomModel = [];
times(n, index => {
if (!index) {
dataZoomModel.push(0);
} else if (index === n.length - 1) {
dataZoomModel.push(100 - stepNumber);
} else if (index === 1) {
dataZoomModel.push(dataZoomModel[index - 1] + stepNumber);
} else {
dataZoomModel.push(dataZoomModel[index - 1] + stepNumber * 2);
}
});
return dataZoomModel;
}
export function sizeMathSmaller(startOne, startTwo, n) {
let multiplier = 0.9;
if (n) {
times(n, index => {
multiplier += Math.pow(0.1, index + 1) * 0.9;
});
}
const result = startTwo * multiplier;
if (result > startOne) {
return result;
}
sizeMathSmaller(startOne, startTwo, n + 1);
}
sizeMathSmaller是什么?
我们知道,start是闭合区间,end是非闭合区间,它无限接近下一个start值
export function sizeMathSmaller(startOne, startTwo, n) {
let multiplier = 0.9;
if (n) {
times(n, index => {
multiplier += Math.pow(0.1, index + 1) * 0.9;
});
}
const result = startTwo * multiplier;
if (result > startOne) {
return result;
}
sizeMathSmaller(startOne, startTwo, n + 1);
}
递归,算出一个合理的end值。
虽然start和end在上文说到,是个区间,但是它最后还是会被转换成startValue和endValue进行下标绑定(无关数据多少是否完整)
两个解决方案:
为什么有两个解决方案,因为要看业务需求的要求。
当我们业务需求要求展示空数值柱子或null值要预留柱子位置时。便只能使用【我的项目中的解决方案 】(在下面会详述。)因为我们无法将series剪切成我们想要的数据(掐头去尾不可用,二维表定位置空或null不可用。)
如果我们业务需求不要求展示空数值柱子或null值要预留柱子位置时。我们可以实现与官方datazoom滑动的效果。思路如下:
代理datazoom事件-判断上下滚-定位数据下标-计算出下标对应的start和end-通过记录movetimes获悉所在的当前柱群的数据范围-为每次滑动设置一个数据滚动步进值(可以是半屏或者是一屏幕)-重新赋值start和end(在当前数据下标对应的start和end里计算步进率并获取步进后的start和end)以达到挟持start和end的目的-利用步进的start和end剪切数据,并把非当前屏幕的数据置空(null)-渲染echarts
我的项目中的解决方案
设计思路:一个维度独占一屏幕或整数屏。同个屏幕不会出现两个柱子群。如果维度翻页后不足一屏也以最终数量倒推使其占满一屏。
代理datazoom事件-判断上下滚-定位数据下标-计算出下标对应的start和end-通过记录movetimes获悉所在的当前柱群的数据范围重新赋值start和end以达到挟持start和end的目的-利用start和end和movetimes剪切数据-渲染echarts
....
if (!nowPageNum) {
// 为0时是最后一屏
end = seriesData.length + 1;
start = end - maxPageBar;
} else {
start = maxPageBar * (nowPageNum - 1);
end = start + maxPageBar;
}
return seriesData.slice(start, end);
设置start和value,并用Echart.on 监听代理datazoom事件,并通过返回的event事件里包含的start和end值对比原先的start和end判断上下滑。
/**
* 监听dataZoom鼠标事件上下滚动
* @param { string } zoomId: dataZoom的id
* @param { Object } eventData echarts事件对象
* @param { Object } preEventData echarts事件对象
* @return { Number }
*/
export function listenDataZoomEvent(zoomId, eventData, preData) {
const { batch = [] } = eventData;
// 错误事件
if (batch.length < 1) return 0;
const batchDataZoom = batch.find(item => item.dataZoomId === zoomId);
if (!batchDataZoom || !preData) return 0;
if (batchDataZoom.end > preData.end && batchDataZoom.start > preData.start) {
// 向下
return 1;
} else if (
preData.end > batchDataZoom.end &&
preData.start > batchDataZoom.start
) {
// 向上
return -1;
} else {
return 0;
}
}
可以开始监听datazoom事件了
this.myChart.off('datazoom');
// 代理datazoom事件
this.myChart.on('datazoom', event => {
const options = this.myChart.getOption();
// 判断上下滚动
let moveTime = 0;
if (!this.preViewZoomEvent) {
moveTime = listenDataZoomEvent(options.dataZoom[0].id, event, {
start: printData.dataZoom[0].start,
end: printData.dataZoom[0].end,
});
} else {
moveTime = listenDataZoomEvent(
options.dataZoom[0].id,
event,
this.preViewZoomEvent,
);
}
const newOption = deepClone(options);
const { zoomPageTime, groupNum, zoomTimes } = options.customDataZoom;
if (zoomTimes + moveTime < 0) {
newOption.customDataZoom.zoomTimes = 0;
} else if (zoomTimes + moveTime >= groupNum * zoomPageTime) {
newOption.customDataZoom.zoomTimes = groupNum * zoomPageTime - 1;
} else {
newOption.customDataZoom.zoomTimes += moveTime;
}
newOption.series = handleSeries(
newOption.customDataZoom,
printData.series,
);
const newZoom = countNewDataZoom(newOption.customDataZoom);
newOption.dataZoom = newOption.dataZoom.map(item => {
if (item.id === options.dataZoom[0].id) {
item.start = newZoom.start;
item.end = newZoom.end;
return item;
} else {
return item;
}
});
this.drawDataZoom(newOption);
this.preViewZoomEvent = {
start: newZoom.start,
end: newZoom.end,
};
});
} else {
this.myChart.setOption(printData, {
notMerge: true,
});
}
dataZoom的作用是帮我们判断上下滑,除此之外无任何实际意义,我们通过movetimes来记录翻了多少页,(并把它塞到chartoption里进行储存)从而计算数据的定位范围
customDataZoom: {
inCustomDataZoom,
zoomTimes: 0,
// 每一个柱群有多少页
zoomPageTime,
// 一共有多少柱群
groupNum,
// 每一屏的最大柱子叔
maxPageBar: barColumn,
dataZoomModel,
},
剪切数据和强制绑定datazoom的start和end从而劫持datazoom event事件
/**
* 计算customDataZoom的start和value
* @param { Object } customDataZoom 自定义滚动信息
* @return { Object }
*/
export function countNewDataZoom(customDataZoom) {
const { zoomTimes, zoomPageTime, dataZoomModel, groupNum } = customDataZoom;
const nowPageNum = zoomTimes + 1;
const seriesIndex = Math.ceil(nowPageNum / zoomPageTime);
const start = dataZoomModel[seriesIndex - 1];
let end;
if (seriesIndex === dataZoomModel.length) {
end = 100;
} else {
end = sizeMathSmaller(start, dataZoomModel[seriesIndex], 0);
}
// const end =
// ((realSeries.length * seriesIndex - 1) /
// (realSeries.length * realSeries[0].data.length)) *
// 100;
// const start =
// (seriesIndex === 1
// ? 0
// : (realSeries.length * (seriesIndex - 1)) /
// (realSeries.length * realSeries[0].data.length)) * 100;
// let start = ((seriesIndex - 1) / groupNum) * 100;
// let end = (seriesIndex / groupNum) * 100 - 1;
// 加一个步进值否则无法向上滚
let stepPercent;
if (zoomPageTime === 1) {
// 写死固定值
stepPercent = (end - start) * 0.1;
} else {
stepPercent = (end - start) / zoomPageTime;
}
// 避免到了最后一个维度无法翻页
if (groupNum * zoomPageTime > nowPageNum && end === 100) {
end = sizeMathSmaller(dataZoomModel[dataZoomModel.length - 1], 100, 0);
}
// const ceilNum = nowPageNum % zoomPageTime;
// 为0时刚好某个维度最后一页
// if (!ceilNum) {
// start = end - stepPercent + 1;
// } else {
// start += (ceilNum - 1) * stepPercent;
// end = start + stepPercent - 1;
// }
return { start: start + stepPercent, end };
}
/**
* 计算customDataZoom的start和value
* @param { Object } customDataZoom 自定义滚动信息
* @param { Object } seriesData 数据源
* @return { Object }
*/
export function handleSeries(customDataZoom, seriesData) {
const { zoomTimes, maxPageBar, zoomPageTime } = customDataZoom;
if (zoomPageTime === 1) {
return seriesData;
}
const nowPageNum = (zoomTimes + 1) % zoomPageTime;
let end;
let start;
if (!nowPageNum) {
// 为0时是最后一屏
end = seriesData.length + 1;
start = end - maxPageBar;
} else {
start = maxPageBar * (nowPageNum - 1);
end = start + maxPageBar;
}
return seriesData.slice(start, end);
}
思路大概如此