likes
comments
collection
share

React 使用Canvas绘制大数据表格

作者站长头像
站长
· 阅读数 10

    之前一直想用Canvas做表格渲染的,最近发现了一个很不错的Canvas绘图框架Leafer,api很友好就试着写了一下。     表格渲染主要分为四个部分,1、表头渲染,2、表格渲染,3、滚动条渲染,4、滚动条与表格的联动。

1、表头渲染

    表头的通过 JSON 格式来设置的,主要包括每列的名称、对应的数据的键值、宽度、是否需要对数据进行二次渲染。     首先需要解决的是表头的正确渲染,这里分为两种情况:         1、表格列都没有设置宽度         2、表格列有设置宽度

1.1、表格列都没有设置宽度

1.1.1、计算表格每列的宽度

    这里已知的是表格的宽度,表格的列数及表格列的名称,解决方案如下:     文本与表格宽度比率 = 表格宽度 / 表格列文本总宽度     每列宽度 = 每列表格文本宽度 * 文本与表格宽度比率     获取文本宽度方法:

const getTextWidth = (leafer: Leafer, text: string) => {
	return leafer.canvas.measureText(text).width;
};

1.1.2、计算表格每列的开始坐标

    初始化表格列数据结构,给表格列添加 width 字符

const thList = columns.map((item) => {
    return {
      ...item,
      width: item.width
        ? item.width
        : Math.floor(getTextWidth(leafer, item.title) * widthRatio),
    };
});

    循环遍历表格列 渲染表头

const group = new Group({ x, y, id: "tableHeader" });

thList.forEach((th, index) => {
	const midLength = thList.slice(0, index).reduce((acc, cur) => {
		return acc + cur.width;
	}, 0);
	const x = index === 0 ? 0 : midLength - index;

	const rect = new Rect({
		x,
		y: 0,
		width: th.width,
		height: initParams.headerHeight,
		fill: "#417A77",
		stroke: "#b4c9fb",
	});
	group.add(rect);
	const text = new Text({
		x,
		y,
		width: th.width,
		textAlign: "center",
		height: headerHeight,
		verticalAlign: "middle",
		fill: "#000000",
		text: th.title,
		fontSize,
	});
	group.add(text);
});

    到这里为止 表头就可以正常渲染出来了

1.2、表格列有设置宽度

    与没有设置表格列的渲染类似     文本与表格宽度比率 = (表格宽度 - 表格设置列的总宽度) / 表格列文本总宽度     没有设置宽度的列宽度 = 每列表格文本宽度 * 文本与表格宽度比率

const noSetWidthColWidth = columns.reduce((acc, cur) => {
    if (cur.width) {
      return "";
    }
    return acc + cur.title;
}, "");
const textWidth = getTextWidth(leafer, noSetWidthColWidth);
const setColWidthSum = columns.reduce((acc, cur) => {
    if (cur.width) {
      return acc + cur.width;
    }
    return acc;
}, 0);

const widthRatio = (width - setColWidthSum) / textWidth;

    渲染方式同上,最后挂载到 leafer 中完成渲染

2、滚动条渲染

    在表格渲染之前要先解决表格滚动条和表格联动的问题,根据滚动条滚动的距离计算表格显示的内容,因为是自绘制表格,所以滚动条部分不能利用浏览器的滚动条。

2.1、创建滚动条

    滚动条的本质还是一个 Rect,使 Rect 模拟滚动条的行为。

const rect = new Rect({
	x: width - scrollBar.width,
	y: initParams.headerHeight,
	width: scrollBar.width - scrollBar.margin * 2,
	height: scrollBar.height,
	fill: "rgba(133,117,85, 0.8)",
	cornerRadius: 10,
	id: "scrollBar",
	zIndex: scrollBar.zIndex,
});

2.2、计算滚动条的高度、位置、样式

2.2.1、计算滚动条的高度

    根据数据量的大小,需要调整滚动条渲染的高度,计算方式如下:     每条数据对应滚动条高度 = (表格总高度 - 表头高度) / 数据长度     滚动条高度 = 滚动条最小高度 + 视图内显示行数 * 每条数据对应滚动条高度

const computedScrollBarHeight = (
	leafer: Leafer,
	dataSource: Record<string, string>[],
	jumpIndex = 0
) => {
	const { height } = leafer;
	const { viewHeight, viewCapacity } = getViewInfo(leafer);
	const unitLength = (height - initParams.headerHeight) / dataSource.length;
	if (jumpIndex) {
      return initParams.scrollBar.height;
	}

	const targetHeight = initParams.scrollBar.height + viewCapacity * unitLength;
	// 小数据量做临时处理
	return targetHeight < viewHeight ? Math.ceil(targetHeight) : viewHeight - 10;
};

2.2.2、滚动条的位置

    滚动条的 X 轴位置 = 表格的宽度 - 滚动条区域的宽度     滚动条的 Y 轴滚动需要添加鼠标滚轮和拖拽事件的监听,对滚动条拖拽事件的监听是通过监听滚动条本身,鼠标滚轮的监听需要对表格本身添加监听事件

leafer.on(MoveEvent.MOVE, function (e) {
	setScroll(leafer, rect, e, dataSource, -0.1, scrollParams);
});

rect.on(DragEvent.DRAG, function (e) {
	setScroll(leafer, rect, e, dataSource, 1, scrollParams);
});

    滚动条的最大滚动高度 = 表格的高度 - 滚动条高度     滚动条的渲染是从设置的坐标点开始 + 滚动条的高度,保证滚动条在可视区域内,需要减去滚动条的高度。     当滚动或拖拽计算值超过最大高度时,为最大高度;当滚动或拖拽计算值小于表头高度时,为表头高度,其他情况为滚动条在 Y 轴方向的偏移值 + 鼠标滚轮滚动的距离或拖拽的距离

const setScroll = (
	leafer: Leafer,
	rect: Rect,
	e: MoveEvent | DragEvent,
	dataSource: Record<string, string>[],
	val = 1,
	scrollInfo: ScrollInfo
) => {
	const {
		scrollMaxHeight,
		headerHeight,
		height,
		scrollBar,
		viewCapacity,
		unitLength,
	} = scrollInfo;
	leafer.children = leafer.children.filter((item) =>
		fixedGroup.includes(item.id ?? "")
	);
	/**
	 * 鼠标滚轮的滚动向上滚动是正值,向下是负值
	 * 这与滚动条位置是相反的,需要在获取滚动距离时 * -1
	 *  */
	rect.y =
		rect.y + e.moveY * val >= scrollMaxHeight
			? scrollMaxHeight
			: rect.y + e.moveY * val < headerHeight
			? headerHeight
			: rect.y + e.moveY * val;
};

2.2.3、滚动条的样式

    滚动条 = 滚动条本身 + 左右边距     滚动条本身宽度 = 滚动条宽度 - 边距 * 2     鼠标移入移出滚动条时会有显隐效果,通过对 Rect 添加移入移出事件来修改透明度

rect.on(PointerEvent.ENTER, (e) => {
	e.target.fill = "rgba(133,117,85, 1)";
});
rect.on(PointerEvent.LEAVE, (e) => {
	e.target.fill = "rgba(133,117,85, 0.8)";
});

2.3、滚动条是否显示

    当数据长度小于可视区域内的行数时,此时不需要出滚动条,在初始化表格调用滚动条方法添加判断。

export const drawCanvasTable = (
	leafer: Leafer,
	columns: Column[],
	dataSource: Record<string, string>[],
	jumpIndex = 0
) => {
	// ...
	dataSource.length > viewCapacity &&
		initScrollBar(leafer, dataSource, jumpIndex);
};

3、表格渲染

3.1、初始化渲染

    表格的渲染类似于表头的渲染,表格的渲染是按照行来渲染,每行的列坐标、宽度是和表头一样的,可以在表格渲染的部分保存一份。

thList.forEach((th, index) => {
	const midLength = thList.slice(0, index).reduce((acc, cur) => {
		return acc + cur.width;
	}, 0);
	const x = index === 0 ? 0 : midLength - index;
	tableHeaderInfo[th.dataIndex] = {
		x,
		width: th.width,
	};
	// ...省略渲染部分...
});

3.2、获取渲染的范围

    表格渲染内容的起始位置是通过滚动条位置来计算的,并通过滚动条位置的变化来重新渲染表格。     滚动距离等于最大滚动距离时,渲染的起始位置为数据总长度 - 视图可显示的行数。     滚动距离小于最大滚动距离时:         滚动条偏移范围内需要渲染的数据单位长度 = (表格高度 - 表格头高度 - 滚动条高度) / (数据长度 - 视图可显示行数)         渲染的起始位置 = (滚动条位置 - 表格头高度) / 滚动条偏移范围内需要渲染的数据单位长度

const setScroll = (
	leafer: Leafer,
	rect: Rect,
	e: MoveEvent | DragEvent,
	dataSource: Record<string, string>[],
	val = 1,
	scrollInfo: ScrollInfo
) => {
	// ...计算滚动条位置代码...

	from =
     rect.y === scrollMaxHeight
       ? dataSource.length - viewCapacity
       : Math.ceil((rect.y - headerHeight) / unitLength);

	initTableBody();
};

    当数据大于表格视图行数时,表格结束范围 = 起始位置 + 表格视图可以显示的最大行数,如果计算值大于数据最大长度,则为数据长度,否则表格的结束范围 = 数据长度

const computedViewBoundary = (
	i: number,
	start: number,
	viewCapacity: number,
	dataSource: Record<string, string>[]
) => {
	if (dataSource.length > viewCapacity) {
     return (i < start + viewCapacity && start + viewCapacity <= dataSource.length);
	} else {
     return i < dataSource.length;
	}
};

3.3、计算表格 Y 轴方向的偏移

    因为计算视图内可以显示的表格行时,会有小数的存在,这里是采用向下取整,这样显示的行数总高度会超过表格的可视区域高度,这时候需要对表格进行部分偏移,使其在滚动到底部时能够正常显示     1、数据长度小于可视区域行数时,不需要偏移     2、数据长度大于可视区域行数时         2.1 开始位置小于需要随滚动条渲染的数据长度时,不需要偏移         2.2 开始位置大于等于需要随滚动条渲染的数据长度时:             2.2.1 如果表格行高可以被视图高度整除,不需要偏移             2.2.2 如果表格行高不可以被视图高度整除,偏移值 = 表格头高度 - (表格行高 - 视图高度 % 行高)

const computedTableOffset = (
	viewCapacity: number,
	headerHeight: number,
	viewHeight: number,
	rowHeight: number
) => {
	return globalDataSource.length > viewCapacity
     ? from < Math.floor(globalDataSource.length - viewCapacity)
	     ? headerHeight
		: headerHeight -
	         (viewHeight % rowHeight ? rowHeight - (viewHeight % rowHeight) : 0)
	     : headerHeight;
};

4、跳转到指定位置

    当表格数据量大时,需要能够快速定位到某条数据,当接收到需要跳转到的行时,该数据为起始位置,重新执行渲染表格一系列方法,因为在滚动条初始化时修改了滚动条的初始高度,所以在跳转操作时不应该修改表格行的高度

useEffect(() => {
	if (canvasDom.current) {
		const leafer = new Leafer({
			view: canvasDom.current,
			width: 500,
			height: 800,
			move: { dragOut: false },
			type: "user",
		});

		drawCanvasTable(leafer, columns, dataSource, jumpIndex);
	}
}, [columns, dataSource, jumpIndex]);
if (jumpIndex) {
	return initParams.scrollBar.height;
}

代码地址:

    stackblitz.com/edit/vitejs…