
在使用 d3.js 创建柱状图时,尤其是在处理具有序数(如年份、类别)数据的 x 轴时,一个常见的问题是柱体(bar)无法精确地与 x 轴上的对应刻度线对齐。例如,一个表示“1880”年的柱体可能会偏离“1880”刻度线若干像素。这通常发生在图表需要响应式布局,并且使用了 d3.scale.ordinal().rangeroundbands() 来分配柱体空间时。
d3.scale.ordinal().rangeRoundBands(interval, padding) 方法旨在为序数数据提供一个连续的区间,并将其划分为等宽的“带”(bands),每个带对应一个数据点。rangeRoundBands 会返回一个函数,当你传入一个数据域中的值时,它会返回该值对应带的起始位置(即左边缘的 X 坐标)。同时,它还提供了 rangeBand() 方法来获取每个带的宽度,这通常被用作柱体的宽度。
问题症结在于,x(d.Year) 返回的是柱体所占据空间的起始 X 坐标,而不是其中心 X 坐标。如果直接将此值赋给柱体的 x 属性,柱体就会从其带的左边缘开始绘制,从而导致与刻度线(通常位于带的中心)不对齐。
要使柱体的中心精确地对齐 X 轴刻度线,我们需要对柱体的 x 坐标进行微调。由于 x(d.Year) 给出的是带的起始位置,而 x.rangeBand() 给出的是带的宽度(即柱体的宽度),那么柱体的中心位置应该在其起始位置加上其宽度的一半。
然而,更简洁且常用的做法是,将 x(d.Year) 视为刻度线的位置(如果刻度线默认绘制在带的中心),然后将柱体向左移动其宽度的一半。这样,柱体的中心就与 x(d.Year) 的位置对齐了。
具体来说,在设置柱体的 x 属性时,应将 x(d.Year) 减去 x.rangeBand() / 2。
.attr("x", function(d) {
// x(d.Year) 返回的是带的起始位置
// x.rangeBand() / 2 是柱体宽度的一半
// 减去一半宽度,使得柱体中心与 x(d.Year) 返回的位置对齐
return x(d.Year) - x.rangeBand() / 2;
})
.attr("width", x.rangeBand()) // 柱体宽度就是带的宽度注意事项:
下面是应用了上述修正的 D3 柱状图完整代码。此代码将创建一个响应式的柱状图,显示正负温度值,并确保每个柱体与其对应的年份刻度线精确对齐。
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 响应式柱状图:Land Ocean Temperature Index</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<style type="text/css">
html, body, * {
font-family: Arial, sans-serif;
text-align: center;
font-size: 14px; /* 调整基础字体大小 */
}
.bar.positive {
fill: darkred;
}
.bar.negative {
fill: steelblue;
}
g.infowin {
fill: grey;
}
g.infowin text,
.axis text {
font: 11px sans-serif;
fill:grey;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
path.domain {
stroke:none;
}
</style>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript">
// 定义图表边距
var margin = {
top: 10,
right: 10,
bottom: 20,
left: 30
};
// 计算图表实际宽度和高度,使其响应屏幕大小
var width = window.innerWidth - margin.left - margin.right;
var height = window.innerHeight - margin.top - margin.bottom;
// Y 轴比例尺:线性比例尺,映射温度值到像素高度
var y = d3.scale.linear()
.range([height, 0]); // 范围从底部到顶部
// X 轴比例尺:序数比例尺,用于年份数据
// rangeRoundBands 会分配等宽的带,并进行四舍五入以避免半像素渲染问题
// .2 是带之间的填充比例
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .2);
// 用于 X 轴刻度线的线性比例尺 (注意:这里与柱体的 x 比例尺不同,用于显示年份刻度)
// 这是一个潜在的混淆点,通常 X 轴刻度也应使用序数比例尺或基于序数比例尺进行调整
// 为了简化,这里沿用原代码的线性比例尺,但需要注意其刻度生成可能与实际柱体位置的对应关系
// 更推荐的做法是让 xAxisScale 也基于 x 比例尺的 domain
var xAxisScale = d3.scale.linear()
.domain([1880, 2015]) // 原始数据年份范围
.range([0, width]);
// X 轴生成器
var xAxis = d3.svg.axis()
.scale(xAxisScale) // 使用线性比例尺生成刻度
.orient("bottom")
.tickFormat(d3.format("d")); // 整数格式
// Y 轴生成器
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// 创建 SVG 容器
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// 加载 CSV 数据
d3.csv("https://gist.githubusercontent.com/djr-taureau/d3599e17a3f1c5089a10e26dac9aee03/raw/a43e610d8b1c99bc17b8789b5641b84f243871b1/Land-Ocean-TempIndex-YR.csv", type, function(error, data) {
if (error) throw error;
// 设置 X 轴的 domain (数据域)
x.domain(data.map(function(d) {
return d.Year;
}));
// 设置 Y 轴的 domain,并使用 .nice() 使刻度更美观
y.domain(d3.extent(data, function(d) {
return d.Celsius;
})).nice();
// 绘制柱体
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", function(d) {
return d.Celsius < 0 ? "bar negative" : "bar positive";
})
.attr("data-yr", function(d){
return d.Year;
})
.attr("data-c", function(d){
return d.Celsius;
})
.attr("title", function(d){
return (d.Year + ": " + d.Celsius + " °C");
})
.attr("y", function(d) {
// 如果温度为正,柱体从温度值绘制到 Y 轴零点
// 如果温度为负,柱体从 Y 轴零点绘制到温度值
return d.Celsius > 0 ? y(d.Celsius) : y(0);
})
.attr("x", function(d) {
// 核心修正:从带的起始位置减去柱体宽度的一半,使其中心对齐
return x(d.Year) - x.rangeBand() / 2;
})
.attr("width", x.rangeBand()) // 柱体宽度等于带的宽度
.attr("height", function(d) {
// 柱体高度是温度值与零点之间的高度差
return Math.abs(y(d.Celsius) - y(0));
})
.on("mouseover", function(d){
// 鼠标悬停交互
d3.select("#_yr")
.text("Year: " + d.Year);
d3.select("#degrree")
.text(d.Celsius + "°C");
});
// 绘制 Y 轴
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Y 轴标签
svg.append("g")
.attr("class", "y axis")
.append("text")
.text("°Celsius")
.attr("transform", "translate(15, 40), rotate(-90)");
// 绘制 X 轴
// 注意:这里 X 轴的 transform 偏移量需要根据实际情况调整,
// 确保刻度线与柱体中心对齐。
// 原始代码中有一个 (margin.left - 6.5) 的偏移,这可能是为了视觉调整。
// 如果 X 轴刻度也应基于序数比例尺 x,则不需要额外的 margin.left 调整。
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")") // 修正 X 轴位置,使其位于图表底部
.call(xAxis);
// 绘制 X 轴零点线
svg.append("g")
.attr("class", "x axis")
.append("line")
.attr("y1", y(0))
.attr("y2", y(0))
.attr("x2", width);
// 信息窗口(用于鼠标悬停显示数据)
svg.append("g")
.attr("class", "infowin")
.attr("transform", "translate(50, 5)")
.append("text")
.attr("id", "_yr");
svg.append("g")
.attr("class", "infowin")
.attr("transform", "translate(110, 5)")
.append("text")
.attr("id","degrree");
});
// 数据类型转换函数
function type(d) {
d.Celsius = +d.Celsius; // 将温度字符串转换为数字
return d;
}
</script>
</body>
</html>代码解析要点:
通过对 d3.scale.ordinal().rangeRoundBands() 返回的 x 坐标进行简单的数学调整,即 x(d.Year) - x.rangeBand() / 2,我们可以确保 D3 柱状图中的柱体精确地居中于其对应的 X 轴刻度线。这一技巧对于创建专业且视觉上准确的响应式数据可视化至关重要。
最佳实践建议:
以上就是D3 响应式柱状图:确保柱体与刻度线精确对齐的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号