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

D3.js热力图实现Y轴可滚动X轴固定功能失败问题求解

问题原因
  • 滚动属性加错了位置:SVG 内部的 <g> 元素不支持 CSS overflow-y: scroll 属性,滚动能力需要给包裹内容的 HTML 块级元素添加才能生效。
  • X 轴固定逻辑错误:SVG 内部元素不支持 HTML 的 position: fixed 定位规则,你给 X 轴文本加这个属性完全不会生效。
  • 可视区域不匹配:Y 轴的 range 设为了 height*6,但外层 SVG 高度只有 500 左右,超出可视区域的内容默认会被 SVG 裁剪,就出现了 Y 轴被截断的问题。
修复方案

核心思路是把固定的 X 轴和可滚动的热力图/Y 轴拆成两个独立部分,上下对齐布局:

  1. 调整 HTML 结构,新增两个容器:顶部固定容器放 X 轴 SVG,下方固定高度的可滚动容器放热力图 SVG
  2. 拆分 D3 绘制逻辑:X 轴单独绘制在顶部 SVG 中,Y 轴和热力块绘制在下方可滚动容器内的 SVG 中,保证两者的宽度、左右 margin 完全一致,实现刻度对齐
完整修改代码
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<!-- 固定X轴容器 -->
<div id="x-axis-container"></div>
<!-- 可滚动热力图容器 -->
<div id="heatmap-container" style="height: 500px; overflow-y: scroll;"></div>
const margin = { top: 10, right: 20, bottom: 30, left: 150 };
const width = 960 - margin.left - margin.right;
// 可视区域高度
const viewHeight = 500 - margin.top - margin.bottom;
// Y轴总高度,适配100+国家
const totalYHeight = viewHeight * 6;

// 绘制顶部固定X轴
const xSvg = d3.select("#x-axis-container")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", 60) // 只留X轴需要的高度
  .append("g")
  .attr("transform", `translate(${margin.left}, 10)`);

// 绘制可滚动区域的SVG
const heatmapSvg = d3.select("#heatmap-container")
  .append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", totalYHeight + margin.top + margin.bottom)
  .append("g")
  .attr("transform", `translate(${margin.left}, ${margin.top})`);

// 加载数据
d3.csv("https://raw.githubusercontent.com/thedivtagguy/daily-data/master/dd_cropYieldsD3/cropYieldsD3/data/land_use.csv").then(function(data) {
  const years = Array.from(new Set(data.map(d => d.year)));
  const countries = Array.from(new Set(data.map(d => d.entity)));

  // 按土地利用变化量降序排列国家
  const sortedCountries = countries.sort((a, b) => {
    const aChange = data.filter(d => d.entity === a).map(d => Number(d.change)).reduce((a, b) => a + b, 0);
    const bChange = data.filter(d => d.entity === b).map(d => Number(d.change)).reduce((a, b) => a + b, 0);
    return aChange - bChange;
  });

  const x = d3.scaleBand()
    .range([0, width])
    .domain(years)
    .padding(0.1);
  
  const y = d3.scaleBand()
    .range([totalYHeight, 0])
    .domain(sortedCountries)
    .padding(0.1);

  // 绘制固定X轴
  xSvg.append("g")
    .call(d3.axisBottom(x).tickValues(years.filter((d, i) => !(i % 10))))
    .selectAll("text")
    .style("color", "black")
    .attr("transform", "translate(-10,10)rotate(-45)")
    .style("text-anchor", "end");

  // 绘制Y轴
  heatmapSvg.append("g")
    .call(d3.axisLeft(y))
    .selectAll("text")
    .style("color", "black")
    .attr("transform", "translate(-10,0)")
    .style("text-anchor", "end");    

  const colorScale = d3.scaleSequential()
    .domain([0, d3.max(data, d => Number(d.change))])
    .interpolator(d3.interpolateInferno);

  // 绘制热力块
  heatmapSvg.selectAll("rect.heat-block")
    .data(data, d => `${d.year}:${d.entity}`)
    .join("rect")
    .attr("class", "heat-block")
    .attr("x", d => x(d.year))
    .attr("y", d => y(d.entity))
    .attr("rx", 4)
    .attr("ry", 4)
    .attr("width", x.bandwidth())
    .attr("height", y.bandwidth())
    .style("fill", d => colorScale(Number(d.change)))
    .style("stroke-width", 4)
    .style("stroke", "none")
    .style("opacity", 0.8);
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 13:45:03