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

React中Highcharts自定义:修复Plotline文本截断及新数据点标记

问题描述

我正在用Highcharts结合highcharts-react-official开发ETH实时价格图表,从父组件获取startedRoundData展示当前轮次详情,已为startTime和endTime添加plotlines,但plotline文本出现截断问题(见附图)。我希望复刻目标图表的所有细节与功能,以下是我的代码:

React组件代码

import React, { useEffect, useState } from "react";
import Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import moment from "moment";

const Chart = ({ data, startedRoundData }) => {
  const [chartOptions, setChartOptions] = useState({});

  useEffect(() => {
    const pricesData = data?.prices || [];
    const chartData = pricesData
      .slice(0, -1)
      .map((item) => [item.timestamp * 1000, parseFloat(item.price)]);

    const lastTimestamp = chartData[chartData.length - 1]?.[0] || 0;
    const futureTimestamp = lastTimestamp + 10000; // 10 seconds in the future

    const options = {
      chart: {
        type: "spline",
        backgroundColor: "#131722",
        height: "400px",
        marginRight: 10,
        marginLeft: 0,
        marginBottom: 30,
        marginTop: 10,
        animation: Highcharts.svg,
      },
      title: null,
      xAxis: {
        type: "datetime",
        lineColor: "#2a2e39",
        tickColor: "#2a2e39",
        labels: {
          style: { color: "#787b86", fontSize: "10px" },
          y: 20,
          formatter: function () {
            return moment(this.value).format("HH:mm:ss");
          },
        },
        tickLength: 0,
        minPadding: 0,
        maxPadding: 0,
        gridLineWidth: 1,
        gridLineColor: "#2a2e39",
        tickInterval: 10000, // 10 seconds interval
        tickAmount: 6,
        min: lastTimestamp - 50000, // Show 50 seconds of past data
        max: futureTimestamp, // Extend to 10 seconds in the future
        plotLines: [
          {
            color: "#ff9800",
            width: 1,
            value: startedRoundData?.startTime
              ? moment(startedRoundData?.startTime).utc().valueOf()
              : null, // Convert to UTC
            dashStyle: "dash",
            zIndex: 5,
            label: {
              useHTML: true, // Allows for custom HTML
              align: "center",
              y: 0,
              x: 0,
              formatter: function () {
                return `
                      <div class="custom-plotline-label">
                        <span class="label-text">Current</span>
                      </div>
                    `;
              },
            },
            events: {
              render: function () {
                // Access and style after rendering
                const plotLineLabel = this.plotLinesAndBands[0].label.element;
                plotLineLabel.style.transform = "rotate(270deg)";
                plotLineLabel.style.transformOrigin = "100% 12px";
                plotLineLabel.style.position = "absolute";
                plotLineLabel.style.left = "150px";
                plotLineLabel.style.top = "10px";
              },
            },
          },

          {
            color: "#ff9800",
            width: 1,
            value: startedRoundData?.lockTime
              ? moment(startedRoundData?.lockTime).utc().valueOf()
              : null, // Convert to UTC
            dashStyle: "dash",
            zIndex: 5,
            label: {
              text: "End",
              align: "center",
              style: { color: "#ffffff", fontSize: "12px" },
              y: 15,
            },
          },
        ],
      },
      yAxis: {
        title: null,
        labels: {
          align: "right",
          x: 45,
          style: { color: "#787b86", fontSize: "10px" },
          formatter: function () {
            return this.value.toFixed(2);
          },
        },
        gridLineColor: "#2a2e39",
        gridLineWidth: 1,
        tickAmount: 5,
        opposite: true,
        plotLines: [
          {
            color: "#ff9800",
            width: 1,
            value: startedRoundData?.startTime
              ? moment(startedRoundData?.startTime).utc().valueOf()
              : null, // Convert to UTC
            dashStyle: "dash",
            zIndex: 5,
            label: {
              text: startedRoundData?.startPrice
                ? startedRoundData?.startPrice
                : null,
              align: "center",
              style: {
                color: "#ffffff",
                fontSize: "12px",
                whiteSpace: "nowrap", // To ensure the text doesn't break
              },
              y: 15,
            },
          },
        ],
      },
      legend: { enabled: false },
      series: [
        {
          name: "Price",
          data: chartData,
          color: "#00ff00",
          lineWidth: 2,
          marker: {
            enabled: false,
          },
        },
      ],
      tooltip: { enabled: false },
      credits: { enabled: false },
      plotOptions: {
        spline: {
          animation: {
            duration: 1000,
            easing: "easeOutQuart",
          },
        },
      },
    };

    setChartOptions(options);
  }, [data]);

  useEffect(() => {
    if (chartOptions.series) {
      const chart = Highcharts.charts[0];
      if (chart) {
        const series = chart.series[0];
        const lastPoint = series.data[series.data.length - 1];
        if (lastPoint) {
          lastPoint.update(
            {
              marker: {
                enabled: true,
                radius: 5,
                symbol: "circle",
                fillColor: "#00ff00",
                lineColor: "#00ff00",
                lineWidth: 2,
                states: {
                  hover: {
                    enabled: true,
                    radius: 5,
                    fillColor: "#00ff00",
                    lineColor: "#00ff00",
                    lineWidth: 2,
                  },
                },
              },
            },
            false
          );
          chart.redraw();
        }
      }
    }
  }, [chartOptions]);

  return (
    <div className="bg-[#131722] p-4 rounded-lg h-[485px]">
      <HighchartsReact highcharts={Highcharts} options={chartOptions} />
    </div>
  );
};

export default Chart;

依赖脚本

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>

解决方案

1. 修复Plotline文本截断问题

  • X轴"Current"标签:
    1. 调整图表marginLeft为40,给旋转后的标签留出左侧空间;
    2. 移除硬编码的left:150px,改用更合理的定位逻辑,同时确保标签不超出容器:
      label: {
        useHTML: true,
        align: "center",
        y: 200, // 垂直居中
        x: -20, // 左移避免超出容器
        style: {
          whiteSpace: 'nowrap',
          overflow: 'visible'
        },
        formatter: function () {
          return `
            <div class="custom-plotline-label" style="white-space: nowrap;">
              <span class="label-text">Current</span>
            </div>
          `;
        },
      },
      events: {
        render: function () {
          const plotLineLabel = this.plotLinesAndBands[0].label.element;
          plotLineLabel.style.transform = "rotate(270deg)";
          plotLineLabel.style.transformOrigin = "center center";
          plotLineLabel.style.position = "absolute";
        },
      },
      
  • Y轴plotline修正:你错误地将时间戳作为Y轴plotline的value,应该替换为价格值,同时调整标签对齐方式避免截断:
    yAxis: {
      // ...其他配置
      plotLines: [
        {
          color: "#ff9800",
          width: 1,
          value: startedRoundData?.startPrice ? parseFloat(startedRoundData.startPrice) : null,
          dashStyle: "dash",
          zIndex: 5,
          label: {
            text: startedRoundData?.startPrice ? startedRoundData.startPrice : null,
            align: "right",
            style: {
              color: "#ffffff",
              fontSize: "12px",
              whiteSpace: "nowrap",
              overflow: "visible"
            },
            y: 0,
            x: -10
          },
        },
      ],
    },
    
  • 通用调整:把图表marginRight改为40,给右侧标签留出显示空间。

2. 代码优化与细节完善

  • 实时数据更新优化:将最后一个点的marker更新逻辑依赖改为data,避免不必要的重绘:
    useEffect(() => {
      const chart = Highcharts.charts[0];
      if (chart && chart.series.length) {
        const series = chart.series[0];
        const lastPoint = series.data[series.data.length - 1];
        if (lastPoint) {
          lastPoint.update(
            {
              marker: {
                enabled: true,
                radius: 5,
                symbol: "circle",
                fillColor: "#00ff00",
                lineColor: "#00ff00",
                lineWidth: 2,
                states: {
                  hover: {
                    enabled: true,
                    radius: 5,
                    fillColor: "#00ff00",
                    lineColor: "#00ff00",
                    lineWidth: 2,
                  },
                },
              },
            },
            false
          );
          chart.redraw();
        }
      }
    }, [data]);
    
  • Plotline条件渲染:当startedRoundData为空时,不渲染plotlines,避免空值异常:
    plotLines: [
      ...(startedRoundData?.startTime ? [{/* startTime plotline配置 */}] : []),
      ...(startedRoundData?.lockTime ? [{/* lockTime plotline配置 */}] : [])
    ]
    
  • X轴范围动态修正:直接取最新数据的时间戳计算范围,确保图表始终显示最近50秒+未来10秒:
    const lastTimestamp = pricesData.length ? pricesData[pricesData.length - 1].timestamp * 1000 : 0;
    const futureTimestamp = lastTimestamp + 10000;
    

3. 自定义CSS补充

添加以下CSS确保自定义标签样式生效:

.custom-plotline-label {
  color: #ffffff;
  font-size: 12px;
  padding: 2px 4px;
  background-color: #ff9800;
  border-radius: 2px;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 03:35:55