BaseChart.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
  2. import Taro from '@tarojs/taro';
  3. import { Canvas } from '@tarojs/components';
  4. import uChartsClass from '../lib/u-charts-original';
  5. import type { ChartsConfig, TouchEvent } from '../types/u-charts-original';
  6. import type { ExtendedCanvasContext } from '../types';
  7. /**
  8. * BaseChart 组件的 Props 接口
  9. * 使用原始 u-charts.js + Canvas 2D API
  10. */
  11. export interface BaseChartProps {
  12. /** Canvas 元素的 ID,必须唯一 */
  13. canvasId: string;
  14. /** 图表宽度(像素),默认为 750 */
  15. width?: number;
  16. /** 图表高度(像素),默认为 500 */
  17. height?: number;
  18. /** 图表类型 */
  19. type: ChartsConfig['type'];
  20. /** X 轴分类数据 */
  21. categories?: string[];
  22. /** 系列数据 */
  23. series?: ChartsConfig['series'];
  24. /** 额外的图表配置 */
  25. config?: Partial<ChartsConfig>;
  26. /** 触摸开始事件 */
  27. onTouchStart?: (e: TouchEvent) => void;
  28. /** 触摸移动事件 */
  29. onTouchMove?: (e: TouchEvent) => void;
  30. /** 触摸结束事件 */
  31. onTouchEnd?: (e: TouchEvent) => void;
  32. }
  33. /**
  34. * BaseChartInner 内部组件
  35. *
  36. * 实际的图表渲染组件,只在数据准备好后才会挂载
  37. * 使用空依赖数组确保只初始化一次
  38. */
  39. const BaseChartInner: React.FC<BaseChartProps> = (props) => {
  40. const {
  41. canvasId,
  42. width = 750,
  43. height = 500,
  44. type,
  45. categories = [],
  46. series = [],
  47. config = {},
  48. onTouchStart,
  49. onTouchMove,
  50. onTouchEnd,
  51. } = props;
  52. const [cWidth, setCWidth] = useState(750);
  53. const [cHeight, setCHeight] = useState(500);
  54. const chartRef = useRef<any>(null);
  55. /**
  56. * 初始化图表实例
  57. * 使用 Canvas 2D API + 原始 u-charts.js
  58. * 参考 ColumnChartFCExample 的实现方式
  59. *
  60. * 注意:使用空依赖数组,只在组件首次挂载时执行一次
  61. * 数据变化通过 Wrapper 组件控制重新挂载来实现
  62. */
  63. useLayoutEffect(() => {
  64. console.debug('[BaseChart] useLayoutEffect 开始', { canvasId, width, height });
  65. // 计算响应式尺寸
  66. const sysInfo = Taro.getSystemInfoSync();
  67. // 这里的第一个 750 对应 css .charts 的 width
  68. const cw = width / 750 * sysInfo.windowWidth;
  69. // 这里的 500 对应 css .charts 的 height
  70. const ch = height / 750 * sysInfo.windowWidth;
  71. setCWidth(cw);
  72. setCHeight(ch);
  73. // 确保数据已准备好
  74. // 对于饼图(pie/ring),只需要 series 有数据
  75. // 对于其他图表,需要 categories 和 series 都有数据
  76. const isPieChart = type === 'pie' || type === 'ring';
  77. const isDataReady = isPieChart ? series.length > 0 : categories.length > 0 && series.length > 0;
  78. if (!isDataReady) {
  79. console.log('[BaseChart] 数据未准备好,等待数据...', {
  80. canvasId,
  81. type,
  82. isPieChart,
  83. categoriesLength: categories.length,
  84. seriesLength: series.length
  85. });
  86. return;
  87. }
  88. // 延迟初始化图表,等待 Canvas 尺寸渲染完成(参考 FC 示例)
  89. setTimeout(() => {
  90. // 使用 Canvas 2D API 的方式获取 context
  91. const query = Taro.createSelectorQuery();
  92. query.select('#' + canvasId).fields({ node: true, size: true }).exec((res) => {
  93. if (res[0]) {
  94. const canvas = res[0].node;
  95. const ctx = canvas.getContext('2d');
  96. console.debug('[BaseChartOriginal2D] canvas.width', canvas.width);
  97. console.debug('[BaseChartOriginal2D] canvas.height', canvas.height);
  98. // 将 Taro CanvasContext 转换为 uCharts 需要的 CanvasContext
  99. const extendedCtx = ctx as ExtendedCanvasContext;
  100. // Canvas 2D: 使用 canvas 的实际 width/height
  101. // 基础配置
  102. const chartConfig: ChartsConfig = {
  103. type,
  104. context: extendedCtx,
  105. categories,
  106. series,
  107. width: canvas.width,
  108. height: canvas.height,
  109. animation: true,
  110. background: '#FFFFFF',
  111. color: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ef4444'],
  112. padding: [15, 15, 0, 5],
  113. enableScroll: false,
  114. legend: {},
  115. xAxis: {
  116. disableGrid: true
  117. },
  118. yAxis: {
  119. data: [{ min: 0 }]
  120. },
  121. ...config,
  122. };
  123. chartRef.current = new uChartsClass(chartConfig);
  124. console.log('[BaseChart] 图表初始化完成:', canvasId, {
  125. cWidth, cHeight,
  126. canvasWidth: canvas.width,
  127. canvasHeight: canvas.height,
  128. categoriesLength: categories.length,
  129. seriesLength: series.length,
  130. categories,
  131. series,
  132. });
  133. } else {
  134. console.error('[BaseChart] 未获取到 canvas node:', canvasId);
  135. }
  136. });
  137. }, 500); // 延迟 500ms,等待 Canvas 尺寸渲染完成
  138. }, []); // 空依赖数组:只在首次挂载时执行一次
  139. /**
  140. * 触摸事件处理
  141. */
  142. const handleTouchStart = (e: any) => {
  143. if (chartRef.current) {
  144. chartRef.current.touchLegend(e);
  145. chartRef.current.showToolTip(e);
  146. }
  147. onTouchStart?.(e as TouchEvent);
  148. };
  149. const handleTouchMove = (e: any) => {
  150. if (chartRef.current) {
  151. chartRef.current.scroll(e);
  152. }
  153. onTouchMove?.(e as TouchEvent);
  154. };
  155. const handleTouchEnd = (e: any) => {
  156. if (chartRef.current) {
  157. chartRef.current.touchLegend(e);
  158. chartRef.current.showToolTip(e);
  159. }
  160. onTouchEnd?.(e as TouchEvent);
  161. };
  162. const canvasProps = { style: { width: cWidth, height: cHeight } };
  163. return (
  164. <Canvas
  165. canvas-id={canvasId}
  166. id={canvasId}
  167. {...canvasProps}
  168. onTouchStart={handleTouchStart}
  169. onTouchMove={handleTouchMove}
  170. onTouchEnd={handleTouchEnd}
  171. type="2d"
  172. />
  173. );
  174. };
  175. /**
  176. * BaseChart 组件
  177. *
  178. * 外层 Wrapper 组件,负责:
  179. * - 检查数据是否准备好
  180. * - 缓存 config 引用
  181. * - 只有数据准备好后才挂载 Inner 组件
  182. *
  183. * 使用原始 u-charts.js + Canvas 2D API
  184. * 参考 docs/小程序图表库示例/taro-2d柱状图使用示例.md
  185. */
  186. export const BaseChart: React.FC<BaseChartProps> = (props) => {
  187. const {
  188. canvasId,
  189. width,
  190. height,
  191. type,
  192. categories = [],
  193. series = [],
  194. config = {},
  195. onTouchStart,
  196. onTouchMove,
  197. onTouchEnd,
  198. } = props;
  199. // 缓存配置,避免每次渲染创建新对象
  200. const stableConfig = useMemo(() => config, [JSON.stringify(config)]);
  201. // 只有数据准备好才渲染 Inner 组件
  202. // 对于饼图(pie/ring),只需要 series 有数据
  203. // 对于其他图表,需要 categories 和 series 都有数据
  204. const isPieChart = type === 'pie' || type === 'ring';
  205. const isReady = isPieChart ? series.length > 0 : categories.length > 0 && series.length > 0;
  206. if (!isReady) {
  207. return null;
  208. }
  209. return (
  210. <BaseChartInner
  211. canvasId={canvasId}
  212. width={width}
  213. height={height}
  214. type={type}
  215. categories={categories}
  216. series={series}
  217. config={stableConfig}
  218. onTouchStart={onTouchStart}
  219. onTouchMove={onTouchMove}
  220. onTouchEnd={onTouchEnd}
  221. />
  222. );
  223. };
  224. export default BaseChart;