You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React项目中如何在手绘图表上标注最高点与最低点?

React环境下图表极值标注实现方案与库推荐

一、专用库推荐

直接用成熟图表库可以大幅减少自定义逻辑,以下是适配React生态的最优选择:

  • Recharts:React专属轻量图表库,原生支持自定义组件标注,API贴合React语法,上手成本低
  • Chart.js + react-chartjs-2:通用图表库的React封装,自带数据极值计算能力,搭配chartjs-plugin-annotation插件可快速实现标注
  • D3.js:高度定制化可视化库,适合复杂手绘场景,完全控制绘图与标注逻辑

二、快速实现方案(以Recharts为例)

适合基于结构化数据的图表场景,代码直接复用:

1. 安装依赖

npm install recharts

2. 核心代码

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Dot, Text } from 'recharts';

// 替换为用户绘制生成的实际数据集
const chartData = [
  { name: 'Mon', value: 120 },
  { name: 'Tue', value: 200 },
  { name: 'Wed', value: 150 },
  { name: 'Thu', value: 80 }, // 最小值
  { name: 'Fri', value: 250 }, // 最大值
  { name: 'Sat', value: 180 },
  { name: 'Sun', value: 220 },
];

// 计算极值点
const values = chartData.map(item => item.value);
const maxVal = Math.max(...values);
const minVal = Math.min(...values);
const maxPoint = chartData.find(item => item.value === maxVal);
const minPoint = chartData.find(item => item.value === minVal);

export default function ExtremesMarkedChart() {
  return (
    <ResponsiveContainer width="100%" height={400}>
      <LineChart data={chartData}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="name" />
        <YAxis />
        <Tooltip />
        {/* 基础折线 */}
        <Line type="monotone" dataKey="value" stroke="#8884d8" strokeWidth={2} />
        {/* 标注最大值点+数值 */}
        <Dot
          cx={(chartData.indexOf(maxPoint) / (chartData.length - 1)) * 800}
          cy={(1 - maxVal / (maxVal * 1.2)) * 400}
          r={6}
          fill="#ff4d4f"
          stroke="#fff"
          strokeWidth={2}
        />
        <Text x={(chartData.indexOf(maxPoint) / (chartData.length - 1)) * 800 + 10} y={(1 - maxVal / (maxVal * 1.2)) * 400 - 10} fill="#ff4d4f">
          {maxVal}
        </Text>
        {/* 标注最小值点+数值 */}
        <Dot
          cx={(chartData.indexOf(minPoint) / (chartData.length - 1)) * 800}
          cy={(1 - minVal / (maxVal * 1.2)) * 400}
          r={6}
          fill="#52c41a"
          stroke="#fff"
          strokeWidth={2}
        />
        <Text x={(chartData.indexOf(minPoint) / (chartData.length - 1)) * 800 + 10} y={(1 - minVal / (maxVal * 1.2)) * 400 + 20} fill="#52c41a">
          {minVal}
        </Text>
      </LineChart>
    </ResponsiveContainer>
  );
}

三、自由手绘场景实现(Canvas)

如果是用户在画布上自由绘制曲线,需手动处理坐标记录与极值标注:

import { useRef, useEffect, useState } from 'react';

export default function FreeDrawWithExtremes() {
  const canvasRef = useRef(null);
  const [drawPoints, setDrawPoints] = useState([]);
  const [isDrawing, setIsDrawing] = useState(false);

  // 开始绘制
  const startDraw = (e) => {
    const canvas = canvasRef.current;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setIsDrawing(true);
    setDrawPoints(prev => [...prev, {x, y}]);
    const ctx = canvas.getContext('2d');
    ctx.beginPath();
    ctx.moveTo(x, y);
  };

  // 绘制中
  const onDraw = (e) => {
    if (!isDrawing) return;
    const canvas = canvasRef.current;
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setDrawPoints(prev => [...prev, {x, y}]);
    const ctx = canvas.getContext('2d');
    ctx.lineTo(x, y);
    ctx.strokeStyle = '#8884d8';
    ctx.lineWidth = 2;
    ctx.stroke();
  };

  // 结束绘制并标注极值
  const stopDraw = () => {
    setIsDrawing(false);
    markExtremePoints();
  };

  // 标注极值点
  const markExtremePoints = () => {
    if (drawPoints.length === 0) return;
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    // Canvas y轴向下,y值越小越靠上(即最大值)
    const maxPoint = drawPoints.reduce((prev, curr) => curr.y < prev.y ? curr : prev);
    const minPoint = drawPoints.reduce((prev, curr) => curr.y > prev.y ? curr : prev);

    // 标注最大值
    ctx.fillStyle = '#ff4d4f';
    ctx.beginPath();
    ctx.arc(maxPoint.x, maxPoint.y, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.fillStyle = '#333';
    ctx.font = '14px Arial';
    ctx.fillText(`最高值`, maxPoint.x + 10, maxPoint.y - 10);

    // 标注最小值
    ctx.fillStyle = '#52c41a';
    ctx.beginPath();
    ctx.arc(minPoint.x, minPoint.y, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.fillText(`最低值`, minPoint.x + 10, minPoint.y + 20);
  };

  return (
    <canvas
      ref={canvasRef}
      width={800}
      height={400}
      style={{ border: '1px solid #ccc' }}
      onMouseDown={startDraw}
      onMouseMove={onDraw}
      onMouseUp={stopDraw}
      onMouseLeave={stopDraw}
    />
  );
}

四、关键注意事项

  • 结构化数据场景优先用Recharts/Chart.js,避免重复造轮子
  • 自由手绘场景需注意Canvas坐标系统(y轴向下,极值判断时需反转逻辑)
  • 可结合useCallback优化绘制性能,避免频繁重渲染

内容的提问来源于stack exchange,提问作者Somebody

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 10:12:47