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

如何为D3折线图零点上下区域设置纯色区分显示?

问题描述

最近刚上手D3.js,正在把原有Chart.js的图表迁移过来,大部分都成功了,但有个折线图没法实现和Chart.js一致的效果。

在Chart.js里可以通过内置的fill属性设置折线零点上下区域的填充色,代码示例:

...config,
fill: {
  above: '#4E4AFF20',
  below: '#FF515114',
  target: 'origin'
},
...config

对应的效果是折线在零点上方的区域用浅蓝填充,下方用浅红填充,线条对应区域也是纯色。但D3.js没有类似内置功能,当前实现的是渐变效果,和预期不符,需要解决两个问题:

  • 实现Chart.js那样的零点上下区域分色填充
  • 线条和填充区域使用纯色而非渐变过渡
解决方案

D3.js需要手动处理零点上下的路径分割,分别绘制填充区域和线条,核心思路是拆分数据为零点上下的独立分段,为每个分段设置对应纯色,具体实现如下:

修改后的完整代码

import * as d3 from 'd3';
import { useEffect, useRef } from 'react';

interface Data {
  x: number;
  y: number;
}

const width = 350;
const height = 117;
const zeroPoint = 0;
const data: Data[] = [
  { x: 0, y: -20 },
  { x: 10, y: -20 },
  { x: 20, y: -20 },
  { x: 40, y: -20 },
  { x: 50, y: -20 },
  { x: 60, y: -20 },
  { x: 70, y: -20 },
  { x: 80, y: 0 },
  { x: 90, y: 20 },
  { x: 100, y: 20 },
  { x: 110, y: 20 },
  { x: 120, y: 20 },
  { x: 130, y: 20 },
  { x: 140, y: 20 },
  { x: 150, y: 20 }
];

// 拆分数据为零点上下的分段,补充跨零点的交点
const splitDataByZero = (data: Data[], zeroY: number) => {
  const segments: { data: Data[], type: 'above' | 'below' | 'zero' }[] = [];
  let currentSegment: Data[] = [];

  for (let i = 0; i < data.length; i++) {
    const curr = data[i];
    const next = data[i + 1];

    currentSegment.push(curr);

    // 处理跨零点的情况:计算折线与零点线的交点
    if (next && ((curr.y < zeroY && next.y > zeroY) || (curr.y > zeroY && next.y < zeroY))) {
      const t = (zeroY - curr.y) / (next.y - curr.y);
      const intersectionX = curr.x + t * (next.x - curr.x);
      const intersection = { x: intersectionX, y: zeroY };

      currentSegment.push(intersection);
      segments.push({ data: [...currentSegment], type: curr.y < zeroY ? 'below' : 'above' });
      currentSegment = [intersection];
    } 
    // 处理到达零点的分段切换
    else if (curr.y === zeroY && next && next.y !== zeroY) {
      segments.push({ data: [...currentSegment], type: 'zero' });
      currentSegment = [curr];
    }
    // 处理分段结束
    else if (i === data.length - 1 || (next && ((curr.y >= zeroY && next.y < zeroY) || (curr.y <= zeroY && next.y > zeroY)))) {
      const type = curr.y > zeroY ? 'above' : curr.y < zeroY ? 'below' : 'zero';
      segments.push({ data: [...currentSegment], type });
      currentSegment = [];
    }
  }

  return segments;
};

// 生成连接到零点线的填充区域路径
const generateAreaPath = (data: Data[], xScale: d3.ScaleLinear<number, number>, yScale: d3.ScaleLinear<number, number>, zeroY: number) => {
  const areaGenerator = d3.area<Data>()
    .x(d => xScale(d.x))
    .y0(yScale(zeroY))
    .y1(d => yScale(d.y));
  return areaGenerator(data);
};

// 生成线条路径
const generateLinePath = (data: Data[], xScale: d3.ScaleLinear<number, number>, yScale: d3.ScaleLinear<number, number>) => {
  const lineGenerator = d3.line<Data>()
    .x(d => xScale(d.x))
    .y(d => yScale(d.y));
  return lineGenerator(data);
};

export const Chart: React.FC = () => {
  const ref = useRef<SVGSVGElement>(null);

  const drawZeroLine = (svg: d3.Selection<SVGSVGElement, unknown, null, undefined>, yScale: d3.ScaleLinear<number, number>) => {
    svg
      .append('line')
      .attr('x1', '0')
      .attr('y1', yScale(zeroPoint))
      .attr('x2', width)
      .attr('y2', yScale(zeroPoint))
      .attr('stroke', '#c4c4c4');
  };

  const createChart = (data: Data[]) => {
    const svg = d3.select(ref.current!).attr('viewBox', `0 0 ${width} ${height}`);
    svg.selectAll('*').remove();

    const [minX, maxX] = d3.extent(data, d => d.x);
    const [minY, maxY] = d3.extent(data, d => d.y);

    const xScale = d3.scaleLinear().domain([minX!, maxX!]).range([0, width]);
    const yScale = d3.scaleLinear()
      .domain([minY!, maxY!])
      .range([height, 0]);

    // 拆分数据为零点上下的独立分段
    const segments = splitDataByZero(data, zeroPoint);

    // 绘制填充区域
    segments.forEach(segment => {
      if (segment.type === 'above' || segment.type === 'below') {
        const fillColor = segment.type === 'above' ? '#4E4AFF20' : '#FF515114';
        svg.append('path')
          .attr('d', generateAreaPath(segment.data, xScale, yScale, zeroPoint))
          .attr('fill', fillColor)
          .attr('stroke', 'none');
      }
    });

    // 绘制线条
    segments.forEach(segment => {
      if (segment.type === 'above' || segment.type === 'below') {
        const strokeColor = segment.type === 'above' ? '#4E4AFF' : '#FF5151';
        svg.append('path')
          .attr('d', generateLinePath(segment.data, xScale, yScale))
          .attr('stroke', strokeColor)
          .attr('stroke-width', 2)
          .attr('fill', 'none');
      } else if (segment.type === 'zero') {
        svg.append('path')
          .attr('d', generateLinePath(segment.data, xScale, yScale))
          .attr('stroke', '#c4c4c4')
          .attr('stroke-width', 2)
          .attr('fill', 'none');
      }
    });

    drawZeroLine(svg, yScale);
  };

  useEffect(() => {
    createChart(data);
  }, []);

  return <svg ref={ref} />;
};

关键实现细节

  • 数据拆分:splitDataByZero函数遍历原始数据,识别跨零点的分段,通过线性插值计算折线与零点线的交点,将数据拆分为独立的上方/下方/零点分段
  • 填充区域:使用d3.area生成填充路径,y0固定为零点的y坐标,确保填充区域自动连接到零点线
  • 纯色设置:每个分段单独设置填充色和线条色,完全匹配Chart.js的分色效果,避免渐变过渡

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 22:25:22