D3 使用年份和百分比数组绘制柱状图时,为什么柱子不显示?
结论
代码中有两处会直接影响柱子的显示:x(d.year) 把年份字符串传给了时间比例尺,x.barWidth 引用了不存在的属性。柱高也需要以 y(0) 为基准计算,否则柱子会伸入底部留白。
将年份转换为 Date,把宽度改为 barWidth,再用 y(0) - y(value) 计算柱高,就可以修正这些问题。
为什么原来的代码画不出柱子?
年份字符串不会自动变成对应年份的日期
x 使用 d3.scaleUtc(),定义域中的值是 Date,而数据中的 year 是 '2017' 这样的字符串。
调用 x('2017') 时,字符串会被转换为数值,并作为时间戳 2017 处理,即 Unix epoch 之后的 2017 毫秒,而不是 2017 年。这个时间远早于定义域中的 2017 至 2024 年,计算出的横坐标会落在画布之外。
可以明确构造 UTC 日期:
new Date(Date.UTC(Number(d.year), 0, 1))
其中月份 0 表示一月。
x.barWidth 没有定义
代码声明的是独立变量:
var barWidth = eachTickWidth / 3;
宽度应该写成:
.attr("width", barWidth)
x 是比例尺函数,代码中没有为它设置 barWidth 属性。访问 x.barWidth 会得到 undefined,虽然不会抛出属性不存在的异常,却无法为矩形提供有效宽度。
柱高应以绘图区底部为基准
当前设置下:
y(0) === height - marginBottom // 450
柱子的底部应该位于 450,因此高度应为:
y(0) - y(value)
原来的 height - y(d.percent) 以整个 SVG 的高度为基准,会让柱子向下多延伸 marginBottom。
另外,原代码中 .range([height - marginBottom, marginTop]) 后面的注释有误,实际范围是 [450, 20],不是 [350, 20]。
如何修改代码?
先替换原来的 x 比例尺定义,为首尾柱子留出空间。这里仍使用连续时间轴,因此数据缺少 2020 年时,图上会保留对应的时间间隔。
const x = d3.scaleUtc()
.domain([
new Date(Date.UTC(2016, 6, 1)),
new Date(Date.UTC(2024, 6, 1))
])
.range([marginLeft, width - marginRight]);
定义域的两端各扩展了约半年。请在绘制坐标轴之前完成修改,让坐标轴和柱子使用同一个比例尺范围。
保留原来的 arrMMR 和 barWidth 定义,将绘制柱子的代码替换为:
const data = arrMMR.map(d => ({
date: new Date(Date.UTC(Number(d.year), 0, 1)),
percent: Number(d.percent)
}));
svg.selectAll("rect")
.data(data)
.join("rect")
.attr("x", d => x(d.date) - barWidth / 2)
.attr("y", d => y(d.percent))
.attr("width", barWidth)
.attr("height", d => y(0) - y(d.percent))
.attr("fill", "#69b3a2");
SVG 矩形的 x 表示左边缘。减去 barWidth / 2 后,每根柱子的中心会对齐对应年份的位置;前面增加的时间轴留白可以避免两端柱子越出绘图区。
最后,建议显式获取容器,替换原来的 container.append(...):
document.getElementById("container").append(svg.node());
这样就不需要依赖浏览器将元素 ID 暴露为全局变量。
还有哪些容易误判的地方?
selectAll("mybar") 不是本次问题的直接原因。即使它没有匹配任何元素,后续的 .data(...).enter().append("rect") 仍然可以创建矩形。使用 selectAll("rect") 可以让选择器与实际创建的元素一致,也便于后续更新。
线性比例尺可以将 percent 中的 '0.98' 等字符串转换为数字,所以它们不是柱子消失的主要原因。提前使用 Number() 转换,可以让数据类型更明确。
如果希望纵轴显示 98% 而不是 0.98,可以把原来的纵轴调用改为:
.call(d3.axisLeft(y).tickFormat(d3.format(".0%")));
数据仍然保持在 0 到 1 之间,不需要再乘以 100。
备注:内容仅供参考。
