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

如何在ReCharts图表中用虚线连接断开的线段?

用ReCharts实现虚线连接断开的线段

我用React搭配ReCharts开发图表,当前图表中的线段存在断开间隔,希望用虚线连接这些分离的线段。

当前问题效果:
图表线段间存在空白间隔

期望实现效果:
期望的虚线连接效果

我希望仅通过ReCharts库完成该需求,尽量不安装额外库,以下是我的代码:

const calculateYearAndMonth = (data: LrpAnalysisPriceTrendsChartData[]) => {
  const yearCounter: Counter = {};
  const monthCounter: Counter = {};
  data.forEach((element) => {
    const year = element.name.slice(4, 8);
    const yearAndMonth = element.name;

    !yearCounter[year] ? (yearCounter[year] = 1) : (yearCounter[year] += 1);
    !monthCounter[yearAndMonth] ? (monthCounter[yearAndMonth] = 1) : (monthCounter[yearAndMonth] += 1);
  });
  return {
    yearCount: yearCounter,
    monthCount: monthCounter
  };
};

const tooltipRenderer =
  (selectedMeasure: string, unit: string, options: { id: string; name: string; color?: string }[]) =>
  ({ active, payload }: any) => {
    if (active && payload && payload[0]) {
      return (
        <TooltipContainer>
          <TooltipLabel>{payload[0].payload.endDate}</TooltipLabel>
          {payload.map(({ dataKey, color, value }: any) => {
            let yearInterval: string;
            dataKey === 'historicalPrice' ? (yearInterval = payload[0].payload.yearInterval) : (yearInterval = '');
            return (
              <TooltipRow key={dataKey} color={color}>
                {yearInterval} {selectedMeasure} :
                {` $${value.toFixed(2)}${unit === '$/cwt' ? '/cwt' : unit === '$/head' ? '/head' : ''}`}
              </TooltipRow>
            );
          })}
        </TooltipContainer>
      );
    }
  };

const renderYearTick = (data: LrpAnalysisPriceTrendsChartData[]) => (tickProps: any) => {
  const { x, y, payload } = tickProps;
  const counters: { [key: string]: { count: number; lastMonth: number } } = {};

  data.forEach((endorsementSummary) => {
    if (!counters[endorsementSummary['endMonth'].split('-')[1]]) {
      counters[endorsementSummary['endMonth'].split('-')[1]] = { count: 0, lastMonth: 0 };
      counters[endorsementSummary['endMonth'].split('-')[1]].count = 1;
      counters[endorsementSummary['endMonth'].split('-')[1]].lastMonth = Number(
        endorsementSummary['endMonth'].split('-')[0]
      );
    } else {
      counters[endorsementSummary['endMonth'].split('-')[1]].count += 1;
      counters[endorsementSummary['endMonth'].split('-')[1]].lastMonth = Number(
        endorsementSummary['endMonth'].split('-')[0]
      );
    }
  });

  const { value, offset } = payload;
  const currentMonth = Number(value.split('-')[0]);
  const centerCalculator = (t: number) => {
    switch (t) {
      case 1:
        return x + offset;
      case 2:
        return x;
      case 3:
        return x - offset;
      case 4:
        return x - offset * 2;
      case 5:
        return x - offset * 3;
      case 6:
        return x - offset * 4;
      case 7:
        return x - offset * 5;
      case 8:
        return x - offset * 6;
      case 9:
        return x - offset * 7;
      case 10:
        return x - offset * 8;
      case 11:
        return x - offset * 9;
      case 12:
        return x - offset * 10;
    }
  };

  if (currentMonth === counters[value.split('-')[1]].lastMonth) {
    return (
      <text x={centerCalculator(counters[value.split('-')[1]].count)} y={y} textAnchor="middle" fontSize={14}>{`${
        value.split('-')[1]
      }`}</text>
    );
  }
};

const renderLineTick = (data: LrpAnalysisPriceTrendsChartData[]) => (tickProps: any) => {
  const { x, y, payload, index } = tickProps;
  const counters: { [key: string]: number } = {};
  data.forEach((endorsementSummary) => {
    if (!counters[endorsementSummary['endMonth'].slice(2, 6)]) {
      counters[endorsementSummary['endMonth'].slice(2, 6)] = 1;
    } else {
      counters[endorsementSummary['endMonth'].slice(2, 6)] += 1;
    }
  });
  const { value, offset } = payload;
  const current_month = Number(value.split('-')[0]);
  const isLast = index === data.length - 1;

  if (index === 0 || isLast || current_month === 12) {
    const pathX = Math.floor(isLast || current_month === 12 ? x + offset * 2 : x);
    return <path d={`M${pathX},${y - 13}v${-35}`} stroke="#CCCCCC" />;
  }
  return null;
};
const renderMonthTick = (name: string) => name.slice(0, 3);
interface Props {
  data: LrpAnalysisPriceTrendsChartData[];
  width?: number;
  height?: number;
  gap: number;
  options: { id: string; name: string; color?: string }[];
  unit: number;
  selectedMeasure: { name: string; id: string };
  yearInterval: string;
}

const PriceTrendsChart: React.FC<Props> = ({
  width,
  height,
  data,
  options,
  gap,
  unit,
  selectedMeasure,
  yearInterval
}) => {
  const leftDomain = getDomain(data.map(({ currentPrice, historicalPrice }) => ({ currentPrice, historicalPrice })));
  const [range, setRange] = React.useState({ startIndex: 0, endIndex: data.length - 1 });

  React.useEffect(() => {
    setRange({ startIndex: 0, endIndex: data.length - 1 });
  }, [data.length]);

  const onBrushChange = (e: BrushRange) => {
    setRange(e);
  };

  const dataSliced = [...data.slice(range.startIndex, range.endIndex + 1)];
  const monthAndYearCount = calculateYearAndMonth(dataSliced);

  return (
    <>
      <ResponsiveContainer width="99%">
        <LineChart
          width={width}
          height={height}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 30,
            bottom: 10
          }}
        >
          <YAxis
            label={{
              value: unit === Unit.CWT ? '$/cwt' : unit === Unit.HEAD ? '$/head' : '$',
              position: 'insideLeft',
              angle: -90,
              dy: 30,
              dx: -5,
              fontWeight: 200,
              fontSize: 15
            }}
            yAxisId="left"
            scale="linear"
            tickLine={false}
            tick={{ fontSize: 14 }}
            type="number"
            domain={leftDomain}
            tickFormatter={formatValue(2)}
            interval="preserveStart"
            tickCount={13}
            width={99}
          />
          <XAxis dataKey="name" height={40} tickFormatter={renderMonthTick} tick={{ fontSize: 12 }} />
          <XAxis
            dataKey="endMonth"
            axisLine={false}
            tickLine={false}
            interval={0}
            tick={renderLineTick(data)}
            height={30}
            scale="band"
            xAxisId="month"
          />
          {data[0].currentPrice !== 0 && data[0].endDate !== '' && (
            <XAxis
              dataKey="endMonth"
              axisLine={false}
              tickLine={false}
              interval={0}
              tick={renderYearTick(data)}
              height={35}
              scale="band"
              xAxisId="year"
            />
          )}
          <Tooltip
            content={tooltipRenderer(
              selectedMeasure.name,
              unit === Unit.CWT ? '$/cwt' : unit === Unit.HEAD ? '$/head' : '',
              options
            )}
          />
          <Legend margin={{ top: 20 }} />
          {data[0].currentPrice !== 0 && data[0].endDate !== '' && (
            <Line
              key={'currentPrice'}
              name={selectedMeasure.name}
              type="linear"
              yAxisId={'left'}
              dataKey={'currentPrice'}
              stroke={'#0000FF'}
              strokeWidth={2}
              dot={false}
            />
          )}
          <Line
            key={'historicalPrice'}
            name={yearInterval + ' ' + selectedMeasure.name}
            type="linear"
            yAxisId={'left'}
            dataKey={'historicalPrice'}
            stroke={'#909090'}
            strokeWidth={2}
            dot={false}
          />
        </LineChart>
      </ResponsiveContainer>
    </>
  );
};

解决方案

直接利用ReCharts自带的属性就能实现,不需要额外依赖:

  1. 连接断点:给<Line>组件添加connectNulls={true},这个属性会让ReCharts自动连接数据中空值对应的断开线段。
  2. 设置虚线样式:通过strokeDasharray属性定义虚线的间隔规则,比如"5 5"表示5px实线+5px空白循环,你可以根据需求调整数值(比如"10 3"就是更长的实线加短空白)。

修改后的两个<Line>组件代码如下:

{data[0].currentPrice !== 0 && data[0].endDate !== '' && (
  <Line
    key={'currentPrice'}
    name={selectedMeasure.name}
    type="linear"
    yAxisId={'left'}
    dataKey={'currentPrice'}
    stroke={'#0000FF'}
    strokeWidth={2}
    dot={false}
    connectNulls={true}
    strokeDasharray="5 5"
  />
)}
<Line
  key={'historicalPrice'}
  name={yearInterval + ' ' + selectedMeasure.name}
  type="linear"
  yAxisId={'left'}
  dataKey={'historicalPrice'}
  stroke={'#909090'}
  strokeWidth={2}
  dot={false}
  connectNulls={true}
  strokeDasharray="5 5"
/>

如果你的线段断开不是因为数据中有空值,而是缺少中间的日期数据点,可以先预处理数据:补全缺失的日期条目,将对应价格字段设为null,再用上述属性就能实现虚线连接。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 17:06:50