坐标轴与完整柱状图
🎯 引言
光画柱子还不够,一张正经的图表得有坐标轴刻度,读者才知道每个柱子代表多少。学会这篇文章,你能掌握 D3 画图的 margin 布局约定,用一行代码画出带刻度的坐标轴,最终完成一个完整规范的柱状图。
🧱 margin 约定:给坐标轴留位置
先想一个问题:y 轴刻度“0、50、100”这些数字画在哪里?画在图表内容的左边。如果柱子贴着 SVG 最左边缘画,刻度数字就被挤到画布外面了。
解决办法是 D3 社区的通行做法——margin 约定:SVG 四周留出边距,图表内容画在中间的区域里:
┌──────────────────────── svg ────────────────────────┐
│ margin.top │
│ ┌───────────────────────────┐ │
│ margin │ │ margin.right │
│ .left │ 图表内容区(g 容器) │ │
│ │ │ │
│ └───────────────────────────┘ │
│ margin.bottom │
└──────────────────────────────────────────────────────┘
代码实现是固定套路:算好内容区宽高,添加一个 g 容器平移到内容区左上角,之后所有图形都画在这个 g 里:
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 480 - margin.left - margin.right; // 内容区宽 420
const height = 280 - margin.top - margin.bottom; // 内容区高 230
const svg = d3
.select(svgRef.value)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
之后比例尺的 range 都基于 width 和 height 计算,(0, 0) 就代表内容区左上角,不用操心边距了。
margin 的作用看对比最直观:有 margin 时刻度完整显示,去掉 margin 后坐标轴直接被画布边缘切掉:
🧱 一行代码画出坐标轴
坐标轴上有轴线、刻度线、刻度数字,自己一个个画太麻烦。D3 的坐标轴函数帮我们一键生成,常用的两个:
d3.axisBottom(xScale):刻度朝下的 x 轴。d3.axisLeft(yScale):刻度朝左的 y 轴。
画轴的固定写法:添加一个 g,平移到正确位置,用 call 把轴画进去:
// x 轴画在内容区底部
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
// y 轴画在内容区左侧(g 本身就在左上角,不用平移)
svg.append('g').call(d3.axisLeft(yScale));
call 的意思是“把当前选中的 g 交给坐标轴函数处理”。执行之后,D3 会在 g 里自动生成一堆子元素,大致长这样:
g(坐标轴)
├── 轴线(一条横线)
├── 刻度组 1:小竖线 + 文字“周一”
├── 刻度组 2:小竖线 + 文字“周二”
└── ……
刻度的位置、数字全都根据比例尺自动算好,这就是比例尺和坐标轴配合的方式。
亲自点一下:按钮执行的就是 call(d3.axisBottom(xScale)) 这行代码,坐标轴瞬间生成:
🧱 定制刻度
坐标轴生成后还能调整:
d3.axisLeft(yScale)
.ticks(5) // 期望 5 个刻度
.tickFormat((d) => `${d}件`); // 刻度文字加个单位
.ticks(5):告诉 D3 大约要 5 个刻度,它会自动选出整齐的间隔。.tickFormat():自定义刻度文字,比如给数字加上单位。
对比默认刻度和定制刻度的实际效果:
🛠 完整柱状图
把 margin、比例尺、坐标轴、数据绑定全部组合起来:
<template>
<svg ref="svgRef" class="chart"></svg>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import * as d3 from 'd3';
const svgRef = ref(null);
const data = [
{ day: '周一', value: 120 },
{ day: '周二', value: 200 },
{ day: '周三', value: 150 },
{ day: '周四', value: 80 },
{ day: '周五', value: 70 },
];
onMounted(() => {
const margin = { top: 20, right: 20, bottom: 30, left: 40 };
const width = 480 - margin.left - margin.right;
const height = 280 - margin.top - margin.bottom;
const svg = d3
.select(svgRef.value)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// x 轴比例尺:分类
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.day))
.range([0, width])
.padding(0.2);
// y 轴比例尺:数值
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value)])
.range([height, 0])
.nice();
// 画柱子
svg.selectAll('rect')
.data(data)
.join('rect')
.attr('x', (d) => xScale(d.day))
.attr('y', (d) => yScale(d.value))
.attr('width', xScale.bandwidth())
.attr('height', (d) => height - yScale(d.value))
.attr('fill', '#5470c6');
// 画坐标轴
svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
svg.append('g').call(d3.axisLeft(yScale));
});
</script>
<style scoped>
.chart {
width: 480px;
height: 280px;
}
</style>
注意柱子的两个属性:y 用 yScale(d.value) 算出柱子顶部的位置,height 用 height - yScale(d.value)——内容区总高减去顶部位置,才是柱子本身的高度。
现在每个图形属性都来自比例尺换算,数据换成什么都能正确显示,这就是“完整图表”和“玩具示例”的区别。
这段代码的真实渲染效果如下:
🧾 小节总结
- margin 约定:SVG 四周留边距放坐标轴,内容画在平移过的
g里,range 基于内容区宽高。 axisBottom画 x 轴、axisLeft画 y 轴,固定套路:append('g')+ 平移 +call。- 坐标轴函数会根据比例尺自动生成轴线和所有刻度。
ticks控制刻度数量,tickFormat自定义刻度文字。- 柱子
y = yScale(d.value),height = height - yScale(d.value)。
❓ 知识问答
Q1:刻度文字重叠了怎么办?
常用三种办法:减少 ticks 数量、用 tickFormat 缩短文字(如 1000 显示成 1k)、旋转刻度文字角度。
Q2:横向柱状图怎么画?
对调两个轴即可:x 轴用 scaleLinear(数值)、y 轴用 scaleBand(分类),柱子用 width 表示数值大小。
Q3:为什么坐标轴要用 call,直接写不行吗?
画一个坐标轴要创建轴线、刻度线、文字等一堆元素,call 能把“把这一串操作交给坐标轴函数完成”的动作接在链式调用里,代码更整洁。它是 D3 的惯用写法,照用即可。
Q4:margin 的值怎么定?
看刻度文字的长度。数字大(如 1000000)就把 left 加大,x 轴标签要旋转就把 bottom 加大,没有固定标准,边画边调。
🧪 小练习
把本课的完整柱状图改造成“三门课程的成绩图”:
- 数据改为语文 80、数学 95、英语 88。
- y 轴刻度用
.ticks(5),并通过tickFormat给数字加上“分”字后缀。 - 把柱子颜色改成
#ee6666。
const data = [
{ subject: '语文', score: 80 },
{ subject: '数学', score: 95 },
{ subject: '英语', score: 88 },
];
onMounted(() => {
// 请在这里编写代码
});
🎉 恭喜你已经能画出带坐标轴的正规图表了!下一篇我们学习图形生成器,向折线图和饼图进军。
