016.009.react-chart-components.md 17 KB

Story 016.009: 创建 React 图表组件封装

Status

Approved

Story

作为 小程序开发者, 我想要 创建现代 React 函数式组件封装 u-charts 核心库, 以便 简化图表使用方式,无需手动管理 Canvas 上下文和事件处理,提高开发效率。

背景

u-charts 原库需要手动管理 Canvas 上下文和事件处理,使用类组件的方式较为繁琐。现代小程序开发使用 React 函数式组件和 Hooks,需要创建易于使用的图表组件。

参考实现: docs/小程序图表库示例/使用示例.md 提供了完整的 React + Taro 使用示例,使用的是类组件方式。

前置故事完成状态:

  • ✅ 故事 016.001-016.008: u-charts 核心库已完成模块化搬迁和类型定义
  • ✅ uCharts 类可通过 @d8d/mini-charts 导入使用
  • ✅ 所有核心功能函数已模块化,可供组件导入

前置故事完成状态

故事 016.001: ✅ 已完成 - 创建 mini-charts 包基础结构 故事 016.002: ✅ 已完成 - 模块化 config 和 utils 故事 016.003: ✅ 已完成 - 模块化 data-processing 函数 故事 016.004: ✅ 已完成 - 模块化 charts-data 函数 故事 016.005: ✅ 已完成 - 模块化 renderers 函数 故事 016.006: ✅ 已完成 - 搬迁核心类并完成模块化 故事 016.007: ✅ 已完成 - 搬迁遗漏的辅助函数 故事 016.008: ✅ 已完成 - 搬迁核心绘制函数完成模块化

Acceptance Criteria

  1. 创建 BaseChart 基础组件,封装 Canvas 创建和销毁逻辑
  2. 至少实现5种图表组件(ColumnChart、LineChart、CandleChart、PieChart、RadarChart)
  3. 组件支持 Props 配置,类型定义完整
  4. 组件支持触摸事件处理(tooltip 显示)
  5. 组件支持响应式尺寸计算和像素比适配
  6. 类型定义完整,类型检查通过(pnpm typecheck
  7. 构建成功(pnpm build),自动生成 .d.ts 声明文件

Tasks / Subtasks

  • [ ] Task 1: 创建 BaseChart 基础组件 (AC: 1, 5, 6)

    • 1.1 创建 src/components/BaseChart.tsx 基础组件
    • 1.2 实现 Canvas 创建和销毁逻辑(useEffect cleanup)
    • 1.3 实现响应式尺寸计算(useMemo + Taro.getSystemInfoSync)
    • 1.4 实现像素比适配逻辑
    • 1.5 定义 BaseChartProps 接口(canvasId, width, height, data, config等)
    • 1.6 使用 useRef 管理 uCharts 实例
  • [ ] Task 2: 创建 ColumnChart 柱状图组件 (AC: 2, 3, 4, 6)

    • 2.1 创建 src/components/ColumnChart.tsx
    • 2.2 定义 ColumnChartProps 接口
    • 2.3 实现柱状图数据配置
    • 2.4 实现 tooltip 事件处理(onTouchStart)
    • 2.5 添加类型注解
  • [ ] Task 3: 创建 LineChart 折线图组件 (AC: 2, 3, 4, 6)

    • 3.1 创建 src/components/LineChart.tsx
    • 3.2 定义 LineChartProps 接口
    • 3.3 实现折线图数据配置(支持 dataPointShape、enableScroll等)
    • 3.4 实现拖拽滚动事件处理(onTouchStart、onTouchMove、onTouchEnd)
    • 3.5 添加类型注解
  • [ ] Task 4: 创建 CandleChart K线图组件 (AC: 2, 3, 4, 6)

    • 4.1 创建 src/components/CandleChart.tsx
    • 4.2 定义 CandleChartProps 接口
    • 4.3 实现K线图数据配置(支持移动平均线 MA5/MA10/MA30)
    • 4.4 实现拖拽滚动和tooltip事件处理
    • 4.5 添加类型注解
  • [ ] Task 5: 创建 PieChart 饼图组件 (AC: 2, 3, 4, 6)

    • 5.1 创建 src/components/PieChart.tsx
    • 5.2 定义 PieChartProps 接口
    • 5.3 实现饼图数据配置
    • 5.4 实现tooltip事件处理
    • 5.5 添加类型注解
  • [ ] Task 6: 创建 RadarChart 雷达图组件 (AC: 2, 3, 4, 6)

    • 6.1 创建 src/components/RadarChart.tsx
    • 6.2 定义 RadarChartProps 接口
    • 6.3 实现雷达图数据配置
    • 6.4 实现tooltip事件处理
    • 6.5 添加类型注解
  • [ ] Task 7: 创建组件导出文件 (AC: 3, 6)

    • 7.1 创建 src/components/index.ts
    • 7.2 导出所有图表组件
    • 7.3 导出所有Props类型定义
  • [ ] Task 8: 更新 src/index.ts 主入口 (AC: 3, 6, 7)

    • 8.1 从 components 导出所有图表组件
    • 8.2 导出组件Props类型定义
    • 8.3 更新 package.json 的 exports 字段支持 components 导出
  • [ ] Task 9: 验证组件实现 (AC: 6, 7)

    • 9.1 运行类型检查验证类型注解正确(pnpm typecheck
    • 9.2 运行构建验证生成 .d.ts 文件(pnpm build
    • 9.3 检查生成的 .d.ts 文件正确导出组件类型
  • [ ] Task 10: 创建基础测试(可选) (AC: 3, 4)

    • 10.1 创建测试 Canvas mock
    • 10.2 为 BaseChart 创建基础渲染测试
    • 10.3 为一个图表组件创建基础测试

Dev Notes

前置故事见解

故事 016.001-016.008 完成状态总结:

  • ✅ u-charts 核心库已完全模块化搬迁完成
  • ✅ 所有函数都有完整的 TypeScript 类型注解
  • src/lib/charts/u-charts.ts 导出 uCharts 主类
  • src/lib/charts/u-charts-event.ts 导出 uChartsEvent 事件类
  • ✅ 所有渲染函数、数据处理函数、辅助函数都已模块化
  • ✅ 类型检查通过(pnpm typecheck
  • ✅ 构建成功(pnpm build),自动生成 .d.ts 声明文件
  • ✅ 原始 u-charts.ts 已备份为 u-charts.ts.backup

关键技术决策:

  • 保持代码逻辑完全不变,只改变文件组织方式
  • 只在搬迁过程中添加 TypeScript 类型注解,不修改代码的实现逻辑
  • 使用 ES6 export 语法导出函数和类型

技术栈要求

来源: mini-charts/package.json

  • React: 18.0.0(mini 包使用 React 18,不是 React 19)
  • Taro: 4.1.4(包括 @tarojs/components, @tarojs/react, @tarojs/taro)
  • TypeScript: 5.4.5
  • Node.js: 20.18.3(运行时环境)
  • 包管理器: pnpm workspace

重要: mini 包使用 React 18,不是主项目使用的 React 19。

项目结构指南

来源: source-tree.md

mini-ui-packages/
└── mini-charts/              # mini-charts 包
    ├── src/
    │   ├── index.ts          # 主入口文件(需要更新)
    │   ├── components/       # [本故事创建] React 图表组件
    │   │   ├── BaseChart.tsx         # 基础图表组件
    │   │   ├── ColumnChart.tsx       # 柱状图组件
    │   │   ├── LineChart.tsx         # 折线图组件
    │   │   ├── CandleChart.tsx       # K线图组件
    │   │   ├── PieChart.tsx          # 饼图组件
    │   │   ├── RadarChart.tsx        # 雷达图组件
    │   │   └── index.ts              # 组件导出
    │   └── lib/               # u-charts 核心库(已完成)
    │       ├── config.ts
    │       ├── utils/
    │       ├── data-processing/
    │       ├── charts-data/
    │       ├── renderers/
    │       ├── helper-functions/
    │       ├── draw-controllers/
    │       └── charts/
    │           ├── u-charts-event.ts
    │           ├── u-charts.ts
    │           └── index.ts
    ├── tests/                 # 测试目录
    ├── package.json
    ├── tsconfig.json
    └── jest.config.cjs

TypeScript 配置规范

来源: ui-package-standards.md

{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "module": "ESNext",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  }
}

本故事重要: 需要为所有组件Props接口添加完整的类型注解,确保 strict: true 模式下无类型错误。

uCharts 使用参考

来源: docs/小程序图表库示例/使用示例.md

原始类组件使用方式

import React, { Component } from 'react';
import Taro from '@tarojs/taro';
import { View, Canvas } from '@tarojs/components';
import uCharts from '../../js_sdk/u-charts/u-charts.js';

export default class Index extends Component {
  constructor() {
    super(...arguments)
    this.state = {
      cWidth: '',
      cHeight: '',
      pixelRatio: 1,
    }
  }

  componentDidMount() {
    const sysInfo = Taro.getSystemInfoSync();
    let pixelRatio = 1;
    if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) {
      pixelRatio = sysInfo.pixelRatio;
    }
    const cWidth = pixelRatio * sysInfo.windowWidth;
    const cHeight = 500 / 750 * cWidth;
    this.setState({ cWidth, cHeight, pixelRatio }, () => this.getServerData());
  }

  showColumn = (canvasId, chartData) => {
    const { cWidth, cHeight, pixelRatio } = this.state;
    let ctx = Taro.createCanvasContext(canvasId);
    canvaColumn = new uCharts({
      type: 'column',
      context: ctx,
      legend: true,
      fontSize: 11,
      background: '#FFFFFF',
      pixelRatio,
      animation: true,
      categories: chartData.categories,
      series: chartData.series,
      xAxis: { disableGrid: true },
      yAxis: {},
      dataLabel: true,
      width: cWidth,
      height: cHeight,
      extra: {
        column: {
          type: 'group',
          width: cWidth * 0.45 / chartData.categories.length
        }
      }
    });
  }

  touchColumn = (e) => {
    canvaColumn.showToolTip(e, {
      formatter: function (item, category) {
        return category + ' ' + item.name + ':' + item.data
      }
    });
  }

  render() {
    const { cWidth, cHeight } = this.state;
    return (
      <View>
        <Canvas
          canvas-id="canvasColumn"
          id="canvasColumn"
          onTouchStart={this.touchColumn}
        />
      </View>
    )
  }
}

关键API说明

API 说明
Taro.getSystemInfoSync() 获取系统信息(屏幕宽度、像素比等)
Taro.createCanvasContext(canvasId) 创建Canvas上下文
Taro.getEnv() 获取当前环境(微信/支付宝/H5等)
new uCharts(config) 创建图表实例
chart.showToolTip(e, options) 显示tooltip
chart.scrollStart(e) / chart.scroll(e) / chart.scrollEnd(e) 滚动事件处理

组件设计模式

BaseChart 基础组件

// src/components/BaseChart.tsx
import React, { useEffect, useRef, useMemo } from 'react';
import Taro from '@tarojs/taro';
import { Canvas } from '@tarojs/components';
import { uCharts } from '../lib/charts';

export interface BaseChartProps {
  canvasId: string;
  width?: number;
  height?: number;
  pixelRatio?: number;
  type: string;
  categories: string[];
  series: any[];
  config?: Record<string, any>;
  onTouchStart?: (e: any) => void;
  onTouchMove?: (e: any) => void;
  onTouchEnd?: (e: any) => void;
}

export const BaseChart: React.FC<BaseChartProps> = (props) => {
  const {
    canvasId,
    width,
    height,
    pixelRatio,
    type,
    categories,
    series,
    config = {},
    onTouchStart,
    onTouchMove,
    onTouchEnd,
  } = props;

  const chartRef = useRef<uCharts | null>(null);

  // 计算响应式尺寸
  const { cWidth, cHeight, actualPixelRatio } = useMemo(() => {
    const sysInfo = Taro.getSystemInfoSync();
    const pr = pixelRatio ?? (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY ? sysInfo.pixelRatio : 1);
    const cw = width ?? pr * sysInfo.windowWidth;
    const ch = height ?? (500 / 750 * cw);
    return { cWidth: cw, cHeight: ch, actualPixelRatio: pr };
  }, [width, height, pixelRatio]);

  // Canvas props
  const canvasProps = useMemo(() => {
    if (Taro.getEnv() === Taro.ENV_TYPE.ALIPAY) {
      return {
        width: cWidth,
        height: cHeight,
        style: { width: '100%', height: '100%' }
      };
    }
    return {
      style: { width: `${cWidth}px`, height: `${cHeight}px` }
    };
  }, [cWidth, cHeight]);

  // 初始化图表
  useEffect(() => {
    const ctx = Taro.createCanvasContext(canvasId);
    chartRef.current = new uCharts({
      type,
      context: ctx,
      categories,
      series,
      width: cWidth,
      height: cHeight,
      pixelRatio: actualPixelRatio,
      ...config,
    });

    return () => {
      // 清理图表实例
      chartRef.current = null;
    };
  }, [canvasId, type, categories, series, cWidth, cHeight, actualPixelRatio, config]);

  // 事件处理
  const handleTouchStart = (e: any) => {
    onTouchStart?.(e);
  };

  const handleTouchMove = (e: any) => {
    onTouchMove?.(e);
  };

  const handleTouchEnd = (e: any) => {
    onTouchEnd?.(e);
  };

  return (
    <Canvas
      canvas-id={canvasId}
      id={canvasId}
      {...canvasProps}
      onTouchStart={handleTouchStart}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleTouchEnd}
    />
  );
};

export default BaseChart;

具体图表组件示例

// src/components/ColumnChart.tsx
import React from 'react';
import { BaseChart, BaseChartProps } from './BaseChart';

export interface ColumnChartProps extends Omit<BaseChartProps, 'type'> {
  // 柱状图特有配置
  dataLabel?: boolean;
  columnType?: 'group' | 'stack';
}

export const ColumnChart: React.FC<ColumnChartProps> = (props) => {
  const { dataLabel = true, columnType = 'group', ...baseProps } = props;

  const defaultConfig = {
    legend: true,
    fontSize: 11,
    background: '#FFFFFF',
    animation: true,
    dataLabel,
    xAxis: { disableGrid: true },
    yAxis: {},
    extra: {
      column: {
        type: columnType,
        // width 会在组件内部动态计算
      }
    }
  };

  // tooltip 事件处理
  const handleTouchStart = (e: any) => {
    if (baseProps.onTouchStart) {
      baseProps.onTouchStart(e);
    }
    // 使用 chartRef 调用 showToolTip
  };

  return (
    <BaseChart
      {...baseProps}
      type="column"
      config={{ ...defaultConfig, ...baseProps.config }}
      onTouchStart={handleTouchStart}
    />
  );
};

export default ColumnChart;

类型定义规范

来源: ui-package-standards.md

类型定义原则:

  1. 为所有组件Props定义 TypeScript 接口
  2. 导出所有公共类型定义
  3. 使用泛型增强类型安全性
  4. 避免使用 any 类型(除非必要)

编码标准

来源: coding-standards.md

  • 代码风格: TypeScript 严格模式,一致的缩进和命名
  • 组件类型: 使用 React 函数式组件,使用 React.FC<Props> 类型
  • Hooks: 使用 useEffect、useRef、useMemo 等 Hooks 管理状态和副作用
  • 事件处理: 使用箭头函数或 useCallback 处理事件
  • 导出规范: 使用 ES6 export 语法导出组件和类型

环境配置

来源: CLAUDE.md

  • Node.js: 20.19.2
  • 包管理器: pnpm
  • 测试框架: Jest(mini 包使用 Jest,不是 Vitest)
  • 类型检查: 使用 pnpm typecheck 验证类型
  • 构建: 使用 pnpm build 生成 .d.ts 声明文件

文件位置规范

所有新组件应创建在:

  • 组件目录: mini-ui-packages/mini-charts/src/components/
  • 组件导出: mini-ui-packages/mini-charts/src/components/index.ts
  • 主入口: mini-ui-packages/mini-charts/src/index.ts(需要更新)

技术约束

  1. React版本: 必须使用 React 18 函数式组件,不能使用类组件
  2. Taro版本: 使用 @tarojs/taro 4.1.4 API
  3. 类型安全: 避免使用 any 类型(除非必要)
  4. Canvas上下文: 使用 Taro.createCanvasContext 创建上下文
  5. 事件处理: 正确处理触摸事件,支持 tooltip 和滚动
  6. 响应式设计: 支持不同屏幕尺寸和像素比
  7. 清理逻辑: useEffect cleanup 函数必须清理图表实例

验证标准

完成本故事后,应该满足:

  1. src/components/ 目录下所有文件存在
  2. ✅ BaseChart 和5种图表组件有完整类型注解
  3. src/index.ts 正确导出所有组件
  4. ✅ 运行 pnpm typecheck 无类型错误
  5. ✅ 运行 pnpm build 成功,自动生成 .d.ts 声明文件
  6. ✅ 检查生成的 .d.ts 文件正确导出组件类型
  7. ✅ 组件可以被其他 mini 包引用使用

不包含在本故事中的工作

以下工作不在本故事范围内:

  • ❌ 创建使用示例和文档(故事 016.010)
  • ❌ 创建完整的测试套件(故事 016.011)
  • ❌ 在实际小程序页面中集成使用

Testing

测试框架: Jest(mini 包使用 Jest,不是 Vitest)

测试要求:

  1. 创建基础测试验证组件的正确导入和导出
  2. 测试组件渲染(可选)
  3. 测试 Props 传递(可选)
  4. 测试事件处理(可选)
  5. Mock Taro 和 Canvas 相关API

测试命令:

# 运行所有测试
pnpm test

# 运行特定测试
pnpm test --testNamePattern "图表组件"

测试文件位置: tests/components/

Change Log

Date Version Description Author
2025-12-24 1.0 创建故事文档 Bob (Scrum Master)
2025-12-24 1.1 更新状态为 Approved Bob (Scrum Master)

Dev Agent Record

此部分由开发代理在实施过程中填写

Agent Model Used

Debug Log References

Completion Notes List

File List

QA Results

此部分由 QA 代理在审查完成后填写