F2 图形使用(JSX)完整指南:用标签绘制自定义图表元素
F2 图形使用(JSX)完整指南:用标签绘制自定义图表元素
本指南面向在移动端图表库 F2 中使用 JSX 与图形标签自定义绘制元素的开发者。文章以官方教程 图形使用 - JSX 为主体,结合仓库源码与测试用例,系统讲解从基础图形创建、Class/函数组件封装、props/state 数据传递,到坐标变换、渐变纹理、与 Chart 图表混用以及事件动画的完整实践路径。读完你将掌握用
<group>、<rect>、<circle>、<text>等标签独立构建可复用、可交互、可动画的自定义图形的全部方法,并能通过 HOC(高阶组件)机制深度定制 F2 内置组件。
基础用法:创建自定义图形
F2 底层基于 G 绘图引擎,并完整继承了 @antv/f-engine 的 JSX 能力(见 packages/f2/src/index.ts 中 export * from '@antv/f-engine',以及 jsx-runtime.js)。因此你可以像写 React 一样,用 JSX 标签描述图形结构,再由 Canvas 渲染到 canvas 上。
最简单的自定义图形示例:
/** @jsx jsx */
import { jsx, Canvas } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const Hello = () => {
return (
<group>
<rect
style={{
x: 10,
y: 10,
width: 40,
height: 40,
lineWidth: '2px',
stroke: '#000',
fill: 'red',
}}
/>
<circle
style={{
cx: 80,
cy: 30,
r: 20,
lineWidth: '2px',
stroke: '#000',
fill: 'red',
}}
/>
<text
style={{
x: 120,
y: 30,
text: '文本',
fontSize: 20,
fill: '#000',
}}
/>
</group>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio}>
<Hello />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
关键步骤拆解:
- 文件头部声明
/** @jsx jsx */:告知 Babel/编译工具将 JSX 语法编译为 F2 的jsx函数调用,而不是 React.createElement。 - 创建 2D 上下文:
document.getElementById('container').getContext('2d')拿到 canvas 绘制上下文并传入Canvas。 - JSX 描述图形树:
<group>作为容器承载一组图形;<rect>、<circle>、<text>通过style声明各自的几何与样式属性。 - 渲染:
const { props } = <Canvas .../>取出 props 后new Canvas(props)实例化并调用chart.render()。
这个流程在仓库测试中有完整印证:graphic.test.tsx 中的 图形绘制 用例即用 <group> 包裹 <rect>、<circle>、<polyline> 三个标签渲染后与图片快照比对(toMatchImageSnapshot),并同时验证了函数组件、Class 组件与 Fragment(<>...</>)的绘制行为。
注意:在测试与文档示例中,标签属性既可以写为
style={{...}},也可以写为attrs={{...}},两者在 F2 的 JSX 运行时中均被支持(测试中大量使用attrs写法,如x: '10px'这类带单位的字符串同样合法)。
使用组件:让自定义图形拥有生命周期
直接用函数返回 JSX 适合静态图形;若自定义图形需要走组件渲染管线、拥有生命周期、能监测数据变化,则应使用 F2 的组件机制,详细生命周期见 组件介绍。F2 组件结构与 React 高度一致,支持 Class 组件与函数组件两种写法。
使用 Class 组件
/** @jsx jsx */
import { jsx, Canvas, Component } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
class CustomShape extends Component {
render() {
const { x, y, color } = this.props;
return (
<group>
<rect
style={{
x,
y,
width: 50,
height: 50,
fill: color,
}}
/>
<text
style={{
x: x + 15,
y: y + 30,
text: '自定义',
fontSize: 14,
fill: '#fff',
}}
/>
</group>
);
}
}
const Page = () => {
return (
<group>
<CustomShape x={10} y={10} color="#1890ff" />
<CustomShape x={80} y={10} color="#f5222d" />
<CustomShape x={150} y={10} color="#52c41a" />
</group>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio}>
<Page />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
Class 组件需继承 Component 基类并实现 render() 方法,在 render() 中通过 this.props 读取外部传入的属性。上述示例通过三个 <CustomShape> 实例复用了同一个图形结构,仅传入不同的 x、y、color 即生成不同颜色与位置的自定义图形——这正是组件化"复用性、模块化、扩展性"价值的直接体现。
若需要完整的生命周期钩子,Component 提供 willMount、didMount、shouldUpdate、willReceiveProps、willUpdate、didUpdate、willUnmount、didUnmount 等阶段钩子(见 组件介绍),可用于初始化动画、请求数据、对比 props 决定是否更新、清理定时器等副作用。
使用函数组件
/** @jsx jsx */
import { jsx, Canvas } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const CustomRect = ({ x, y, width, height, color, text }) => {
return (
<group>
<rect
style={{
x,
y,
width,
height,
fill: color,
stroke: '#000',
lineWidth: 2,
}}
/>
<text
style={{
x: x + width / 2 - 20,
y: y + height / 2,
text,
fontSize: 16,
fill: '#fff',
}}
/>
</group>
);
};
const App = () => {
return (
<group>
<CustomRect x={10} y={10} width={80} height={50} color="#1890ff" text="蓝色" />
<CustomRect x={110} y={10} width={80} height={50} color="#f5222d" text="红色" />
</group>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio}>
<App />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
函数组件以 props 解构为入参、直接返回 JSX,无内部状态与生命周期,适合纯展示型、无副作用的图形片段。选型建议:需要状态、生命周期或性能控制(shouldUpdate)时用 Class 组件;纯粹根据输入渲染输出时用函数组件,代码更简洁。
传递数据
通过 props 传递数据
自定义图形可以像组件一样接收任意结构化数据。下面是一个用 props 驱动、纯函数实现的简易柱状图——把数据数组传给外层 Chart 组件,再由 map 逐项生成 Bar:
/** @jsx jsx */
import { jsx, Canvas } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const Bar = ({ data, index, x }) => {
const { name, value } = data;
const height = value * 2;
const y = 200 - height;
return (
<group>
<rect
style={{
x,
y,
width: 40,
height,
fill: index % 2 === 0 ? '#1890ff' : '#f5222d',
}}
/>
<text
style={{
x: x + 10,
y: y - 10,
text: `${value}`,
fontSize: 12,
fill: '#000',
}}
/>
<text
style={{
x: x + 5,
y: 220,
text: name,
fontSize: 12,
fill: '#666',
}}
/>
</group>
);
};
const Chart = ({ data }) => {
return (
<group>
{data.map((item, index) => (
<Bar key={index} data={item} index={index} x={10 + index * 60} />
))}
</group>
);
};
const data = [
{ name: 'A', value: 30 },
{ name: 'B', value: 50 },
{ name: 'C', value: 40 },
{ name: 'D', value: 60 },
];
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio} width={400} height={300}>
<Chart data={data} />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
要点:
key用于列表渲染时的节点复用与更新定位,与 React 语义一致;- 每个
Bar通过data、index、x三个 props 完成"数据 → 几何位置 → 图形"的映射:height = value * 2做简单线性映射,y = 200 - height保证柱体从底部生长; - 外层
Canvas显式指定了width={400} height={300},以便在未引入 Chart 坐标系的情况下精确控制布局。
使用 state 管理状态
若需要响应交互并驱动重绘,Class 组件可使用 this.state + this.setState 管理内部状态,实现真正的"可交互自定义图形":
/** @jsx jsx */
import { jsx, Canvas, Component } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
class InteractiveShape extends Component {
state = {
color: '#1890ff',
scale: 1,
};
handleClick = () => {
this.setState({
color: this.state.color === '#1890ff' ? '#f5222d' : '#1890ff',
});
};
render() {
const { x, y } = this.props;
const { color, scale } = this.state;
const size = 50 * scale;
return (
<group
style={{
x,
y,
cursor: 'pointer',
}}
onTap={this.handleClick}
>
<rect
style={{
x: -size / 2,
y: -size / 2,
width: size,
height: size,
fill: color,
}}
/>
<text
style={{
x: -15,
y: 5,
text: '点击',
fontSize: 14,
fill: '#fff',
}}
/>
</group>
);
}
}
const Page = () => {
return (
<group>
<InteractiveShape x={100} y={100} />
</group>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio} width={300} height={300}>
<Page />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
这里把 cursor: 'pointer' 写在 group 的 style 上提示可点击,并通过 onTap 事件处理器切换颜色。setState 触发组件更新后,render() 会重新执行、图形随之重绘——这就是自定义图形具备交互能力的最小闭环。
使用坐标变换:旋转与缩放
在 group 的 style 中提供 transform 字符串即可对整组图形做变换,语法与 CSS Transform 保持一致:
/** @jsx jsx */
import { jsx, Canvas } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const RotatedRect = ({ x, y, angle, color }) => {
return (
<group
style={{
x,
y,
transform: `rotate(${angle}deg)`,
}}
>
<rect
style={{
x: -25,
y: -25,
width: 50,
height: 50,
fill: color,
}}
/>
</group>
);
};
const App = () => {
return (
<group>
<RotatedRect x={100} y={100} angle={0} color="#1890ff" />
<RotatedRect x={200} y={100} angle={45} color="#f5222d" />
<RotatedRect x={300} y={100} angle={90} color="#52c41a" />
</group>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio} width={400} height={300}>
<App />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
注意两点实现细节:
- 变换施加在
<group>上,group 内的所有子图形会作为一个整体被旋转/缩放; - 矩形以
(x: -25, y: -25, width: 50, height: 50)声明,即以自身中心为原点绘制,配合 group 的x/y定位,旋转时图形绕其中心转动,避免旋转后位置漂移。
F2 的 transform 也支持 scale、translate 等组合,如 transform: 'translate(10px, 20px) scale(1.5)'。同理,rotate 的角度既可以是 45deg 度数,也支持弧度写法,具体格式与 绘图属性 中的约定一致。
使用渐变和纹理
F2 的 fill/stroke 属性除了纯色字符串,还支持与 CSS 用法一致的渐变与纹理写法(完整说明见 绘图属性 - 渐变色与纹理)。
线性渐变
/** @jsx jsx */
import { jsx, Canvas } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const GradientRect = () => {
return (
<rect
style={{
x: 50,
y: 50,
width: 200,
height: 100,
fill: 'linear-gradient(90deg, #1890ff, #f5222d)',
}}
/>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio} width={300} height={200}>
<GradientRect />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
线性渐变方向默认为从左到右(与 Canvas/SVG 保持一致),90deg 表示向右。支持多色阶与颜色百分比锚点,例如 linear-gradient(90deg, blue, green 40%, red),也可以多个渐变叠加。
径向渐变
/** @jsx jsx */
import { jsx, Canvas } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const GradientCircle = () => {
return (
<circle
style={{
cx: 150,
cy: 100,
r: 80,
fill: 'radial-gradient(circle at center, #fff, #1890ff)',
}}
/>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio} width={300} height={200}>
<GradientCircle />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
径向渐变从图形中心向外圈扩散过渡,语法为 radial-gradient(shape at position, ...),如 radial-gradient(circle at center, red, blue, green 100%)。
纹理(Pattern)填充
除字符串渐变外,fill 还接受对象形式的 Pattern,用图片等图案重复填充图形:
<rect
style={{
x: 10,
y: 10,
width: 200,
height: 200,
fill: {
image: 'https://example.com/texture.png',
repetition: 'repeat',
transform: 'rotate(30deg)',
},
}}
/>
Pattern 的类型定义(见 绘图属性)为:
interface Pattern {
image: string | CanvasImageSource | Rect
repetition?: 'repeat' | 'repeat-x' | 'repeat-y' | 'no-repeat'
transform?: string
}
repetition 取值:'repeat' 水平垂直双向重复;'repeat-x' 仅水平重复;'repeat-y' 仅垂直重复;'no-repeat' 不重复。
与图表结合:自定义图表元素
F2 的 Chart 组件是统计图表容器,它会通过 Children.cloneElement 把 data、chart、layout、coord、scaleOptions 等注入到所有子元素中(见 packages/f2/src/chart/index.tsx 的 render() 方法,第 404-413 行)。因此,图形标签可以和 Axis、Interval 等图表组件平级混写在 <Chart> 内,直接复用图表的坐标系与数据上下文,为图表添加自定义标题、注解等元素:
/** @jsx jsx */
import { jsx, Canvas, Chart, Interval, Axis } from '@antv/f2';
const context = document.getElementById('container').getContext('2d');
const data = [
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
];
const Page = () => {
return (
<Chart data={data} scale={{ sold: { min: 0 } }}>
<Axis field="genre" />
<Axis field="sold" />
<Interval x="genre" y="sold" />
{/* 自定义标题 */}
<text
style={{
x: 150,
y: 30,
text: '游戏销量统计',
fontSize: 18,
fill: '#000',
textAlign: 'center',
}}
/>
</Chart>
);
};
const { props } = (
<Canvas context={context} pixelRatio={window.devicePixelRatio} width={300} height={300}>
<Page />
</Canvas>
);
const chart = new Canvas(props);
chart.render();
当自定义图形放在 Chart 内部时,可以进一步借助图表能力:
- 通过
this.props.chart(在 Class 组件中)访问 Chart 实例的公开方法,如chart.getPosition(record)将数据记录映射为画布坐标、chart.getScale(field)获取某个字段的 scale、chart.getSnapRecords(point)获取拾取到的数据记录(这些方法定义在 packages/f2/src/chart/index.tsx 中); - 这样绘制出来的图形会自动"对齐"图表的坐标系、比例尺与布局,真正做到与几何标记(geometry)融为一体。
图形标签与绘图属性速查
自定义图形的表达能力最终取决于图形标签与绘图属性。以下是快速速查,完整属性表见 图形标签 与 绘图属性。
常用图形标签
| 标签 | 用途 | 核心 Style 属性 |
|---|---|---|
<group> |
图形分组,统一管理变换与动画 | x/y、transform |
<rect> |
矩形 | x/y、width/height、radius(圆角,支持 number | number[]) |
<circle> |
圆形 | cx/cy、r |
<sector> |
扇形/环形(饼图、环形图) | cx/cy、r/r0、startAngle/endAngle、anticlockwise |
<polygon> |
多边形 | points: [number, number][] |
<line> |
两点直线 | x1/y1/x2/y2 |
<arc> |
圆弧 | cx/cy、r、startAngle/endAngle |
<polyline> |
多点折线/平滑曲线 | points、smooth |
<text> |
文本 | x/y、text、textAlign、textBaseline、fontSize 等字体属性 |
<image> |
图片 | x/y、width/height、src、cacheImage |
所有图形标签均支持通用属性:className(对象标记)、visible(显隐)、zIndex(绘制层级)、style、animation、以及 onPan/onTap 等事件属性。sector 与 arc 的角度既支持弧度(Math.PI / 2),也支持角度字符串('90 deg')。
Style 绘图属性
F2 中组件样式统一采用 Style 结构,主要分组如下(完整表格见 绘图属性):
- 位置属性:
anchor: [number, number](锚点,默认[0, 0]);不同图形的位置语义不同——Circle/Arc/Sector 用cx/cy表示圆心,Group/Rect/Image 用x/y表示左上角,Text 用x/y表示文本锚点,Line/Polyline/Polygon 用x/y表示包围盒左上角。 - 通用属性:
zIndex(层级,默认0)、clip(裁剪区域)、visibility、opacity(默认1)、fill/stroke(支持纯色、渐变与纹理)、fillOpacity/strokeOpacity(默认1)、阴影系列shadowType/shadowColor/shadowBlur/shadowOffsetX/shadowOffsetY、filter(滤镜)、cursor。 - 线条属性:
lineCap(默认'butt')、lineJoin(默认'miter')、lineWidth(默认1)、miterLimit(默认10)、lineDash(虚线,如[5, 5])。 - 文本属性:
textAlign(默认'start')、textBaseline(默认'alphabetic')、fontStyle/fontSize/fontFamily/fontWeight/fontVariant/lineHeight。
渐变写法速记:linear-gradient(90deg, red, blue) 与 radial-gradient(circle at center, red, blue) 可直接赋给 fill 或 stroke。裁剪写法速记:
<rect
style={{
x: 100,
y: 100,
width: 100,
height: 100,
fill: 'blue',
clip: {
type: 'circle', // 也支持 'rect'、'polygon'
style: {
cx: 150,
cy: 150,
r: 50,
},
},
}}
/>
裁剪区域会同时影响图形的拾取区域,可被多个图形共享。
常见问题
如何让自定义图形支持交互?
在 group 或任意图形标签上直接添加事件处理器即可。F2 5.x 基于 PointerEvent 标准封装了移动端手势事件(完整事件列表见 事件属性),常用事件包括 onClick、onTap、onPan/onPanStart/onPanEnd、onTouchStart/onTouchMove/onTouchEnd、onPressStart/onPress/onPressEnd、onSwipe、onPinchStart/onPinch/onPinchEnd 等:
<group
style={{ cursor: 'pointer' }}
onTap={() => console.log('点击了图形')}
onPress={() => console.log('按住了图形')}
>
<rect style={{ x: 10, y: 10, width: 50, height: 50, fill: 'blue' }} />
</group>
事件处理器定义在 style 之外的标签属性上(如 onTap={...}),配合 style.cursor 可给出视觉反馈。若需要根据数据响应交互,可结合 Class 组件 + state 实现(见上文"使用 state 管理状态"一节)。
如何让自定义图形具有动画效果?
使用 animation 属性。F2 动画定义与 Web Animations API 对齐,每个图形标签都支持基于 Keyframe 的动画(详见 动画属性):
<rect
style={{
x: 10,
y: 10,
width: 50,
height: 50,
fill: 'blue',
animation: {
appear: {
easing: 'linear',
duration: 1000,
property: ['y', 'height'],
start: { y: 200, height: 0 },
end: { y: 10, height: 50 },
},
},
}}
/>
动画按执行阶段分为三类:
appear:初始化时的入场动画(render 阶段触发);update:数据更新时的过渡动画(props 改变时触发);leave:销毁前的离场动画(destroy 阶段触发)。
每个阶段可独立配置 easing、duration、delay、fill、iterations(Infinity 为无限循环)、property(声明需要变换的属性数组)、start/end(开始与结束帧状态)以及 clip(裁剪区域动画)。可变换的属性包括 transform、opacity、fill、stroke、lineWidth、r、width/height、x/y、lineDash、path 等。
缓动函数 easing 默认为 linear,内置大量可选值:恒速类 linear/ease/steps/step-start/step-end,以及 in-sine/out-sine、in-quad/out-quad、in-cubic/out-cubic、in-back/out-back、in-bounce/out-bounce、in-elastic/out-elastic、spring/spring-out 等加速/减速组合。
如何在自定义图形中使用图表的计算逻辑?
若希望自定义元素深度复用图表的 scale、coord、布局等计算逻辑,推荐两条路径:
- 将图形标签直接写在
<Chart>内(见上文"与图表结合"一节),利用 Chart 通过Children.cloneElement向子元素注入data、coord、layout、chart等上下文(源码见 packages/f2/src/chart/index.tsx); - 通过 HOC 自定义 View:F2 将内置组件拆分为
withXXX(逻辑层)与XXXView(渲染层),例如Legend = withLegend(LegendView)。你可以替换渲染层 View 来完全掌控组件外观,同时保留全部计算逻辑与公开方法。详细做法参考 自定义 View。
以自定义图例为例,其核心机制是:withLegend 负责计算 items、处理 filter/highlight 点击逻辑、计算布局并调用 updateCoordFor 为图表预留空间(见 packages/f2/src/components/legend/withLegend.tsx),而自定义 View 只需接收 props 里的 items、itemWidth、style、onClick 并渲染自己的 UI:
import { Canvas, Chart, withLegend } from '@antv/f2';
// 自定义 View:只负责渲染,逻辑全部由 withLegend 提供
const CustomLegendView = (props) => {
const { items } = props;
return (
<group
style={{
flexDirection: 'row',
}}
>
{items.map((item) => {
const { name, color } = item;
return (
<text
style={{
text: name,
fill: color,
}}
/>
);
})}
</group>
);
};
// 用 withLegend 包装自定义 View,得到完整图例组件
const Legend = withLegend(CustomLegendView);
const Page = () => (
<Canvas context={context}>
<Chart data={data}>
<Legend position="top" />
</Chart>
</Canvas>
);
从源码结构可以推断:withXXX 逻辑层通过继承 Component 拥有完整生命周期,并在 willMount/willUpdate 阶段完成布局计算与坐标系占位,因此自定义 View 能拿到"计算逻辑后的结果 props",且无需关心底层坐标系预留等细节。
小结
在 F2 中,JSX 图形标签(group/rect/circle/text 等)构建了自底向上的自定义能力:
- 静态元素:函数组件 + props 即可完成;
- 动态交互:Class 组件 +
state/setState+ 手势事件; - 视觉增强:transform 坐标变换、渐变/纹理填充、animation 阶段动画;
- 融入图表:直接混写在
<Chart>内复用坐标系,或通过withXXXHOC 替换 View 深度定制内置组件。
所有能力均有配套源码与测试可查证:图形绘制的端到端验证见 graphic.test.tsx,Chart 数据注入机制见 packages/f2/src/chart/index.tsx,HOC 封装范式见 packages/f2/src/components/legend/withLegend.tsx。建议结合实际项目边写边跑,逐步从"画出来"走向"算出来、动起来、可复用"。
