D3.js热力图实现Y轴可滚动X轴固定功能失败问题求解
问题原因
- 滚动属性加错了位置:SVG 内部的
<g>元素不支持 CSSoverflow-y: scroll属性,滚动能力需要给包裹内容的 HTML 块级元素添加才能生效。 - X 轴固定逻辑错误:SVG 内部元素不支持 HTML 的
position: fixed定位规则,你给 X 轴文本加这个属性完全不会生效。 - 可视区域不匹配:Y 轴的 range 设为了
height*6,但外层 SVG 高度只有 500 左右,超出可视区域的内容默认会被 SVG 裁剪,就出现了 Y 轴被截断的问题。
修复方案
核心思路是把固定的 X 轴和可滚动的热力图/Y 轴拆成两个独立部分,上下对齐布局:
- 调整 HTML 结构,新增两个容器:顶部固定容器放 X 轴 SVG,下方固定高度的可滚动容器放热力图 SVG
- 拆分 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
相关产品推荐
相关产品推荐

