F2 图形使用(JSX)完整指南:用标签绘制自定义图表元素

原创2026-09-26 12:57:211,607 阅读
文章标签:数据可视化前端

F2 图形使用(JSX)完整指南:用标签绘制自定义图表元素

本指南面向在移动端图表库 F2 中使用 JSX 与图形标签自定义绘制元素的开发者。文章以官方教程 图形使用 - JSX 为主体,结合仓库源码与测试用例,系统讲解从基础图形创建、Class/函数组件封装、props/state 数据传递,到坐标变换、渐变纹理、与 Chart 图表混用以及事件动画的完整实践路径。读完你将掌握用 <group>、<rect>、<circle>、<text> 等标签独立构建可复用、可交互、可动画的自定义图形的全部方法,并能通过 HOC(高阶组件)机制深度定制 F2 内置组件。

F2 图形绘制测试快照,展示 rect、circle、polyline 等标签的组合绘制效果

基础用法:创建自定义图形

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();

关键步骤拆解:

  1. 文件头部声明 /** @jsx jsx */:告知 Babel/编译工具将 JSX 语法编译为 F2 的 jsx 函数调用,而不是 React.createElement。
  2. 创建 2D 上下文:document.getElementById('container').getContext('2d') 拿到 canvas 绘制上下文并传入 Canvas。
  3. JSX 描述图形树:<group> 作为容器承载一组图形;<rect>、<circle>、<text> 通过 style 声明各自的几何与样式属性。
  4. 渲染: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、布局等计算逻辑,推荐两条路径:

  1. 将图形标签直接写在 <Chart> 内(见上文"与图表结合"一节),利用 Chart 通过 Children.cloneElement 向子元素注入 data、coord、layout、chart 等上下文(源码见 packages/f2/src/chart/index.tsx);
  2. 通过 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> 内复用坐标系,或通过 withXXX HOC 替换 View 深度定制内置组件。

所有能力均有配套源码与测试可查证:图形绘制的端到端验证见 graphic.test.tsx,Chart 数据注入机制见 packages/f2/src/chart/index.tsx,HOC 封装范式见 packages/f2/src/components/legend/withLegend.tsx。建议结合实际项目边写边跑,逐步从"画出来"走向"算出来、动起来、可复用"。

相关文档

登录后查看全文
F2