VTable是一款基于可视化渲染引擎VRender的高性能表格组件库,为用户提供卓越的性能和强大的多维分析能力,以及灵活强大的图形能力。相对于dom表格,VTable 基于canvas 画布进行渲染,性能与可视化能力具有碾压级的优势。
介绍文档:VTable——不只是高性能的多维数据分析表格,开源,免费,百万数据秒级渲染
站点:https://visactor.com/vtable
在用户实际的应用场景中,随着需求的不断迭代,对于 VTable 的自定义能力提出来更高的要求。用户期望能够更加灵活、便捷地根据自身的特定需求来定制 VTable 的各项功能和特性,以满足不同业务场景下的多样化需求。为了更好地满足用户的这些需求,VTable 的自定义能力也在不断地进行优化和改进,进行了多个阶段的功能迭代。接下来,我们将分享这部分自定义能力的演进历程和功能展示。
属性配置函数
在VTable最初的版本,我们给用户提供了内容和样式的回调函数式的配置方式:
- 文字内容:在列或指标配置中,fieldFormat配置可以对单元格内容显示进行自定义处理,常用于文字内容的格式化
type FieldFormat = (record: any, col?: number, row?: number, table?: BaseTableAPI) => any;
- 图标:在列或指标配置中,icon配置除了支持固定的图标之外,也支持函数配置,不同单元格显示不同图标
type Icons = (string | ColumnIconOption)[] | ((args: CellInfo) => (string | ColumnIconOption)[]);
- 单元格样式(文字样式,单元格样式):与图标相同,单元格相关的样式都支持函数式配置
{
// 斑马线效果
bgColor: (args: StylePropertyFunctionArg): string {
const { row, table } = args;
const index = row - table.frozenRowCount; // 计算该单元格在body区域的行index
if (!(index & 1)) {
return '#FAF9FB';
}
return '#FDFDFD';
}
// ......
}
- ......
生成单元格节点时,会依据函数的不同返回结果,分别设置各个单元格内的内容和样式。
相关使用案例
const option = {
// style
style: {
bgColor: (arg) => {
return getBgColor(arg.value)
},
color: (arg) => {
return getColor(arg.value)
},
// ......
},
// icon
icon: (arg) => {
if (arg.value >= 0) {
return 'up-icon'
} else if (arg.value < 0) {
return 'down-icon'
}
},
// ......
}
https://www.visactor.com/vtable/demo/business/project-schedule
自定义内容和样式的能力,满足了用户的对单元格内容的调整需求,但是所有的修改都是在单元格原有内容的基础上进行的。部分用户提出了新的需求,希望可以不受单元格类型的限制,自由得绘制内容。基于这部分需求,我们开发了自定义图形(customRender)功能。
图形配置
在全局option、列或指标配置中,可以配置customRender属性,来自由定义单元格内的图形
type ICustomRenderObj = {
/** 配置出来的类型集合 */
elements: ICustomRenderElements;
/** 期望单元格的高度 */
expectedHeight: number;
/** 期望单元格的宽度 */
expectedWidth: number;
/** 是否还需要默认渲染内容 只有配置true才绘制 默认 不绘制 */
renderDefault?: boolean;
};
elements
为图元配置组成的数组,支持下列类型:
- Text
- Rect
- Circle
- Icon
- Image
- Arc
- Line
详细配置可以参考 https://www.visactor.com/vtable/option/ListTable-columns-text#customRender.elements。
为了方便用户设置位置和尺寸,x
y
width
height
等属性支持配置百分比,基于单元格的宽度或高度设置;也可以在属性中使用函数,接收单元格数据,计算相应的属性值。
下面是一个气泡效果的配置:
{
customRender: {
elements: [
{
type: 'circle',
x: '50%', // 定位在单元格中心
y: '50%',
// 依据数据计算圆半径
radius: value => {
const percent = Math.max(5, (Number(value) / 59645 / 2) * 100);
return `${percent}%`;
},
// 依据数据计算渐变色
fill: value => {
const color = getColor(80, 59645, value, 0.5);
const color1 = getColor(80, 59645, value, 1);
return {
gradient: 'linear',
x0: 0,
y0: 1,
x1: 0,
y1: 0,
stops: [
{
offset: 0,
color: color
},
{
offset: 1,
color: color1
}
]
};
}
}
],
renderDefault: false
}
// ......
}
https://www.visactor.com/vtable/demo/business/sales-bubble
相关使用案例
自定义图形功能在常常用在在表格中添加简易按钮,使用自定义图形绘制简单按钮,通过事件监听执行相应的功能
const columns =[
// ......
{
"field": "Operation",
"title": "Operation",
"width": 100,
customRender: {
elements: [
{
type: 'text',
x: '10%',
y: '50%',
textBaseline: 'middle',
fill: '#00c',
text: 'edit',
fontSize: 14,
underline: true,
cursor: 'pointer',
pickable: true
},
{
type: 'text',
x: '50%',
y: '50%',
textBaseline: 'middle',
fill: '#00c',
text: 'del',
fontSize: 14,
underline: true,
cursor: 'pointer',
pickable: true
}
]
}
},
];
另一种用户经常使用的场景,是在单元格内原有数据的基础上,做一些简单的标注
const columns =[
{
"field": "Sales",
"title": "Sales",
"width": "auto",
customRender: {
elements: [
{
type: 'rect',
x: '10%',
y: '10%',
fill: false,
stroke: (value) => value > 200 ? '#c00' : false,
lineWidth: 20,
width: "80%",
height: "80%"
}
],
renderDefault: true
}
},
// ......
];
https://www.visactor.com/vtable/demo/custom-render/custom-render-global
自定义图形详细说明可以参考 https://www.visactor.com/vtable/guide/custom\_define/custom\_render,自定义图形能力,目前推荐使用在单元格原有内容上补充简单图形,或单元格内自定义简单内容的场景
自定义图形功能可以满足在单元格中自由绘制图元,但是在用户使用过程中,随着自定义内容的越来越复杂,发现了一些功能短板:
- 图元需要组织为一个一维数组,复杂场景强制要求扁平化,相应的代码也会很难维护
- 图元布局需要基于单元格绝对定位,没有相对定位能力,也不能自适应布局(flex)
- 内容只能基础绘图图元,对于复杂模块实现比较复杂
- 没有实时更新能力(交互)
自定义场景树节点
针对各类新的复杂功能需求,我们希望提供较为底层的接口,支持用户自行组织VRender场景树节点,来实现单元格内复杂的内容;基于VRender提供的类flex布局能力,用户可以在单元格中进行自适应布局;针对表格中比较常用的一些功能组件(Tag, Chenkbox, Radio),我们也进行了封装,可以很方便的调用。
以一个简单的上下布局的标题标签为例:
import { Group, Text, Tag } from '@visactor/vtable/es/vrender';
// ......
const options = {
columns: [
{
field: 'custom',
title: 'custom-layout',
customLayout: (args) => {
const { table, row, col, rect } = args;
const { height, width } = rect ?? table.getCellRect(col, row);
// 单元格容器
const container = new Group({
height,
width,
display: 'flex',
flexDirection: 'column',
flexWrap: 'nowrap'
});
// 上部group
const containerTop = new Group({
height: height / 2,
width: width,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
fill: 'yellow',
opacity: 0.1,
});
// 上部文字Title
const title = new Text({
text: 'custom-layout-title',
fontSize: 14,
color: '#333',
fontWeight: 600,
boundsPadding: [0, 0, 0, 20]
});
containerTop.add(title);
// 下部group
const containerBottom = new Group({
height: height / 2,
width: width,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
fill: 'green',
opacity: 0.1
});
// 下部tags
const tag1 = new Tag({
text: 'tag1',
textStyle: {
fontSize: 10,
fill: 'rgb(51, 101, 238)'
},
panel: {
visible: true,
fill: '#f4f4f2',
cornerRadius: 5
},
space: 5,
boundsPadding: [0, 0, 0, 20]
});
const tag2 = new Tag({
text: 'tag2',
textStyle: {
fontSize: 10,
fill: 'rgb(51, 101, 238)'
},
panel: {
visible: true,
fill: '#f4f4f2',
cornerRadius: 5
},
space: 5,
boundsPadding: [0, 0, 0, 20]
});
containerBottom.add(tag1);
containerBottom.add(tag2);
container.add(containerTop);
container.add(containerBottom);
return {
rootContainer: container,
renderDefault: false
};
}
},
// ......
],
// ......
}
为了方便定义节点,我们除了实例化后组装的方式外,也支持直接写jsx标签(需要用户的打包环境支持编译jsx),上面的节点也可以写为
const container = (
<VGroup
attribute={{
// ......
}}
>
<VGroup
attribute={{
// ......
}}
>
<VText
attribute={{
// ......
}}
></VText>
</VGroup>
<VGroup
attribute={{
// ......
}}
>
<VTag
attribute={{
// ......
}}
></VTag>
<VTag
attribute={{
// ......
}}
></VTag>
</VGroup>
</VGroup>
)
交互更新
直接操作VRender场景节点后,交互功能就可以使用相应的事件回调很方便的实现
// hover显示icon背景
<VImage
attribute={{
id: 'location-icon',
width: 15,
height: 15,
image,
boundsPadding: [0, 0, 0, 10],
cursor: 'pointer'
}}
stateProxy={stateName => {
if (stateName === 'hover') {
// hover状态更新attribute
return {
background: {
fill: '#ccc',
cornerRadius: 5,
expandX: 1,
expandY: 1
}
};
}
}}
onMouseEnter={event => {
event.currentTarget.addState('hover', true, false);
event.currentTarget.stage.renderNextFrame();
}}
onMouseLeave={event => {
event.currentTarget.removeState('hover', false);
event.currentTarget.stage.renderNextFrame();
}}
></VImage>
相关使用案例
在需要展示富文本内容的场景,使用自定义渲染可以分段组织不同样式的文本,并在单元格中实现对应的布局和展示。
复杂的表格面板也是自定义渲染常用的场景,通过配置不同的图元,可以实现按钮、图标、状态Tag等等的单元格内容。
在表格场景中高度定制展示内容的场景,自定义渲染可以供用户自由实现对应的显示效果。
https://www.visactor.com/vtable/demo/custom-render/custom-cell-layout
自定义渲染详细说明可以参考:https://www.visactor.com/vtable/guide/custom\_define/custom\_layout,推荐使用在单元格内自定义内容较为复杂的场景中
通过函数自定义场景树节点,可以满足大部分用户对于单元格内容的定义的功能,但在用户的开发过程中,一些新的使用问题也开始浮现出来:
- 随着api逐渐底层,自定义功能的上手难度逐渐增加,用户需要对VRender场景树有比较清楚的了解后,才能设计自己的单元格内容场景节点
- 函数式的写法虽然支持jsx标签,但是不是真正的react组件,无法使用props等react组件的基础功能,对于希望可以把单元格内容组件化的用户很不友好
- 一些业务场景,有一些已经完成的高度封装的业务组件,单元格内需要展示react dom组件
针对新的使用问题,我们在这一阶段对react场景进行了专项优化,将单元格内自定义部分进行真正的组件化,并且支持在单元格中展示react dom组件,优化后react开发者可以快速上手自定义组件,也可以支持一部分项目快速迁移。
表格上浮层组件
针对在表格上层实现一个自定义浮层组件(例如tooltip、菜单等),不改变单元格内容的需求,我们提供了全局的CustomComponent
组件,方便快速定位上层组件。
以一个简单的自定义tooltip为例:
function Tooltip(props) {
return (
<div style={{ width: '100%', height: '100%', border: '1px solid #333', backgroundColor: '#ccc', fontSize: 10 }}>
{`${props.value}`}
</div>
);
}
function App() {
const [hoverCol, setHoverCol] = useState(-1);
const [hoverRow, setHoverRow] = useState(-1);
const [value, setValue] = useState('');
const visible = useRef(false);
const tableInstance = useRef(null);
const updateHoverPos = useCallback(args => {
if (visible.current) {
return;
}
setHoverCol(args.col);
setHoverRow(args.row);
const cellValue = tableInstance.current.getCellValue(args.col, args.row);
setValue(cellValue);
}, []);
return (
<ListTable
ref = {tableInstance}
option={option}
onMouseEnterCell={updateHoverPos}
>
<CustomComponent
width="80%"
height="100%"
displayMode="cell"
col={hoverCol}
row={hoverRow}
anchor="bottom-right"
dx="-80%"
>
<Tooltip value={value} />
</CustomComponent>
</ListTable>
);
CustomComponent
组件中,可以依据锚定的单元格的尺寸和位置,自动设置相关样式
- 可以设置相对于单元格的展示位置(anchor)
- 可以依据单元格的尺寸进行百分比设置自身的尺寸
- 可以使用dx dy进行位置微调
表格单元格自定义组件
在jsx标签的基础上,基于react-reconciler
我们对自定义渲染进行了完全的组件封装,用户可以使用提供的图元组件,封装一个真正的react组件。
以一个单元格展示两个Tag的简单组件为例:
function Cell(props) {
const { table, row, col, rect, prefix } = props;
if (!table || row === undefined || col === undefined) {
return null;
}
const { height, width } = rect || table.getCellRect(col, row);
const record = table.getRecordByRowCol(col, row);
return (
<Group
attribute={{
width,
height,
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
justifyContent: 'space-around'
}}
>
<Tag
textStyle={{
fontSize: 14,
fontFamily: 'sans-serif',
fill: 'rgb(51, 101, 238)'
}}
padding={[8, 10]}
panelStyle={{
visible: true,
fill: '#e6fffb',
lineWidth: 1,
cornerRadius: 4
}}
>{`${prefix}-${record.name}-1`}</Tag>
<Tag
textStyle={{
fontSize: 14,
fontFamily: 'sans-serif',
fill: 'rgb(51, 141, 38)'
}}
padding={[8, 10]}
panelStyle={{
visible: true,
fill: '#e6fffb',
lineWidth: 1,
cornerRadius: 4
}}
>{`${prefix}-${record.name}-2`}</Tag>
</Group>
);
}
function App() {
const tableInstance = useRef(null);
const [prefix, setPrefix] = useState('cus');
return (
<ListTable
ref={tableInstance}
records={[{name: 'tag'}]}
>
<ListColumn field={'name'} title={'Tag Component'} width={200}>
<Cell role={'custom-layout'} prefix={prefix}/>
</ListColumn>
</ListTable>
);
}
这里我们自定义了组件Cell
,用来展示两个标签,内容由上层组件传递的props中的prefix和单元格数据决定。这里的组件和传统的组件会有一些区别:
- 组件中使用的标签,必须是react-vtable提供的图元和组件
- 每个列或指标只需要设置一个组件,这个组件会被应用在该列的所有单元格上
- 与
customLayout
的回调函数类似,组件会自带table
,row
,col
,rect
这些props供使用
除了基础的图元组件和Tag Checkbox Radio组件外,react-vtable也内置了Bottom, Link, Avatar和Poptip这些表格中的常用组件
表格单元格中使用react dom组件
如果需要在组件中使用DOM react组件,VRender支持在图元组件的attribute
属性中,指定react
属性,并将react组件作为element
属性传入:
<Group
attribute={{
// ......
react: {
pointerEvents: true,
container: table.bodyDomContainer, // table.headerDomContainer
anchorType: 'bottom-right',
element: <CardInfo record={record} hover={hover} row={row} />
}
}}
>
// ...
</Group>
相应的react组件会在相对于单元格的位置实例化,覆盖在canvas上
目前react dom组件有两种使用方式:
- 在单元格内展示的内容,使用react-vtable提供的图元标签,单元格内触发的弹窗、菜单等组件,可以使用DOM react组件,这是我们推荐的方案。
- 在单元格中完全使用react dom组件,react-vtable也提供完整的表格定位,滚动和更新功能
相关使用实例
使用react dom组件,可以快速将原先react项目中的组件在vtable中展示,并且保留组件的样式和相应的功能。
自定义外部组件——VisActor/VTable react demo
自定义组件——VisActor/VTable react demo
单元格自定义组件+dom组件——VisActor/VTable react demo
单元格内dom组件——VisActor/VTable react demo
模拟飞书人员卡片
- 组件库的补充,增加表格能常用组件,扩展组件库覆盖范围
- 进一步提升自定义组件的性能
- 增加vue版本的自定义组件能力
欢迎更多使用VisActor的用户联系我们,给我们投稿,交流业务场景,提建议,贡献代码,谢谢大家!
VChart :VChart 官网、VChart Github(感谢 Star)
VTable :VTable 官网、VTable Github(感谢 Star)
VMind :VMind 官网、VMind Github(感谢 Star)
官方网站:www.visactor.io/
Discord:discord.gg/3wPyxVyH6m
飞书群:打开链接扫码
微信公众号:打开链接扫码
Twiter:twitter.com/xuanhun1
github:github.com/VisActor