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

如何在D3.js中截断坐标轴.domain类线条的多余部分

解决D3图表中.domain轴线截断问题

我在使用D3构建图表时,需要截断带有.domain类的X轴和Y轴线条的多余部分,让轴线仅保留数据覆盖的范围(当前图表的多余区域已标记,预期效果是轴线两端对齐数据的首尾点)。

解决方案:手动绘制自定义轴线

D3默认生成的.domain轴线是贯穿整个轴的长度,无法直接截断,因此我们可以移除自动生成的domain线,手动计算数据范围对应的坐标来绘制轴线。

代码修改步骤:

  1. 移除自动生成的domain线:
    先删除原有对自动生成.domain元素的样式设置,并移除默认生成的domain线:

    // 注释或删除原有样式设置
    // yAxis.select('.domain').attr('class', 'domain stroke-neutral-600 stroke-[3px]');
    // xAxis.select('.domain').attr('class', 'domain stroke-neutral-600 stroke-[3px]');
    
    // 移除默认生成的domain线
    xAxis.select('.domain').remove();
    yAxis.select('.domain').remove();
    
  2. 绘制自定义X轴domain线:
    计算X轴首尾数据点的中心坐标,绘制仅覆盖数据范围的水平线条:

    const firstX = x(aapl[0].month) + x.bandwidth()/2;
    const lastX = x(aapl[aapl.length-1].month) + x.bandwidth()/2;
    svg.append('line')
      .attr('class', 'domain stroke-neutral-600 stroke-[3px]')
      .attr('x1', firstX)
      .attr('x2', lastX)
      .attr('y1', height - marginBottom)
      .attr('y2', height - marginBottom);
    
  3. 绘制自定义Y轴domain线:
    计算Y轴数据的最大和最小对应坐标,绘制仅覆盖数据范围的垂直线条:

    const yMax = y(d3.max(aapl, d => d.value)*1.05);
    const yMin = y(0);
    svg.append('line')
      .attr('class', 'domain stroke-neutral-600 stroke-[3px]')
      .attr('x1', marginLeft)
      .attr('x2', marginLeft)
      .attr('y1', yMax)
      .attr('y2', yMin);
    

修改后的完整代码

export const App = () => {
  const ref = useRef<SVGSVGElement | null>(null);
  const aapl: DataPoint[] = [
    { month: 'Jan', value: Math.random() * 100 },
    { month: 'Feb', value: Math.random() * 100 },
    { month: 'Mar', value: Math.random() * 100 },
    { month: 'Apr', value: Math.random() * 100 },
    { month: 'May', value: Math.random() * 100 },
    { month: 'Jun', value: Math.random() * 100 },
    { month: 'Jul', value: Math.random() * 100 },
    { month: 'Aug', value: Math.random() * 100 },
    { month: 'Sep', value: Math.random() * 100 },
    { month: 'Oct', value: Math.random() * 100 },
    { month: 'Nov', value: Math.random() * 100 },
    { month: 'Dec', value: Math.random() * 100 },
  ];
  // 图表尺寸与边距
  const width = 928;
  const height = 500;
  const marginTop = 20;
  const marginRight = 30;
  const marginBottom = 30;
  const marginLeft = 40;

  useEffect(() => {
    if (ref.current) {
      // X轴比例尺
      const x = d3
        .scaleBand()
        .domain(aapl.map((d) => d.month))
        .range([0, width - marginRight])
        .padding(0.1);

      // Y轴比例尺
      const y = d3
        .scaleLinear()
        .domain([
          0,
          d3.max(aapl, (d) => d.value * 1.05),
        ] as Iterable<d3.NumberValue>)
        .range([height - marginBottom, marginTop]);

      // 面积生成器
      const area = d3
        .area<DataPoint>()
        .x((d) => {
          const xValue = x(d.month);
          return xValue !== undefined ? xValue + x.bandwidth() / 2 : 0;
        })
        .y0(y(0))
        .y1((d) => y(d.value));

      // 线条生成器
      const line = d3
        .line<DataPoint>()
        .x((d) => {
          const xValue = x(d.month);
          return xValue !== undefined ? xValue + x.bandwidth() / 2 : 0;
        })
        .y((d) => y(d.value));

      // SVG容器
      const svg = d3
        .select(ref.current)
        .attr('width', width)
        .attr('height', height)
        .attr('viewBox', [0, 0, width, height])
        .attr('style', 'max-width: 100%; height: auto;');

      const areaData = area(aapl);
      const lineData = line(aapl);

      // 创建渐变填充
      const lg = svg
        .append('defs')
        .append('linearGradient')
        .attr('id', 'graphGradient')
        .attr('x1', '0%')
        .attr('x2', '0%')
        .attr('y1', '0%')
        .attr('y2', '100%')
        .attr('gradientUnits', 'userSpaceOnUse');

      lg.append('stop').style('stop-color', '#FBB979');
      lg.append('stop').attr('offset', '46%').style('stop-color', '#FFEDD6');
      lg.append('stop')
        .attr('offset', '100%')
        .style('stop-color', '#FFEDD6')
        .style('stop-opacity', '0');

      // 添加面积路径
      svg
        .append('path')
        .attr('fill', 'url(#graphGradient)')
        .attr('d', areaData);

      // 添加线条路径
      svg
        .append('path')
        .attr('fill', 'none')
        .attr('stroke', '#FBB979')
        .attr('stroke-width', 2)
        .attr('d', lineData);

      // 添加X轴
      const xAxis = svg
        .append('g')
        .attr('transform', `translate(0,${height - marginBottom} )`)
        .attr('class', 'x-axis')
        .call(
          d3
            .axisBottom(x)
            .tickSizeInner(0)
            .tickSizeOuter(0)
            .ticks(width / 80)
            .tickPadding(12)
        );

      // 添加Y轴
      const yAxis = svg
        .append('g')
        .attr('transform', `translate(${marginLeft},0)`)
        .attr('class', 'y-axis')
        .call(
          d3
            .axisLeft(y)
            .ticks(height / 40)
            .tickPadding(0)
        );

      // 移除刻度线
      xAxis.selectAll('.tick line').remove();
      yAxis.selectAll('.tick line').remove();
      // 设置刻度文字样式
      yAxis.selectAll('.tick text').attr('class', 'Body Body-XS');
      xAxis.selectAll('.tick text').attr('class', 'Body Body-Small');

      // 移除默认生成的domain线
      xAxis.select('.domain').remove();
      yAxis.select('.domain').remove();

      // 绘制自定义X轴domain线
      const firstX = x(aapl[0].month) + x.bandwidth()/2;
      const lastX = x(aapl[aapl.length-1].month) + x.bandwidth()/2;
      svg.append('line')
        .attr('class', 'domain stroke-neutral-600 stroke-[3px]')
        .attr('x1', firstX)
        .attr('x2', lastX)
        .attr('y1', height - marginBottom)
        .attr('y2', height - marginBottom);

      // 绘制自定义Y轴domain线
      const yMax = y(d3.max(aapl, d => d.value)*1.05);
      const yMin = y(0);
      svg.append('line')
        .attr('class', 'domain stroke-neutral-600 stroke-[3px]')
        .attr('x1', marginLeft)
        .attr('x2', marginLeft)
        .attr('y1', yMax)
        .attr('y2', yMin);
    }
  }, [ref]);

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

内容的提问来源于stack exchange,提问作者João Pedro

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 08:15:56