D3 简介

坐标轴与完整柱状图

学会 margin 布局和坐标轴的绘制,结合比例尺画出一个完整规范的柱状图。

🎯 引言

光画柱子还不够,一张正经的图表得有坐标轴刻度,读者才知道每个柱子代表多少。学会这篇文章,你能掌握 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 都基于 widthheight 计算,(0, 0) 就代表内容区左上角,不用操心边距了。

margin 的作用看对比最直观:有 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、比例尺、坐标轴、数据绑定全部组合起来:

views/BarChart.vue
<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>

注意柱子的两个属性:yyScale(d.value) 算出柱子顶部的位置,heightheight - 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 加大,没有固定标准,边画边调。


🧪 小练习

把本课的完整柱状图改造成“三门课程的成绩图”:

  1. 数据改为语文 80、数学 95、英语 88。
  2. y 轴刻度用 .ticks(5),并通过 tickFormat 给数字加上“分”字后缀。
  3. 把柱子颜色改成 #ee6666
const data = [
    { subject: '语文', score: 80 },
    { subject: '数学', score: 95 },
    { subject: '英语', score: 88 },
];

onMounted(() => {
    // 请在这里编写代码
});

🎉 恭喜你已经能画出带坐标轴的正规图表了!下一篇我们学习图形生成器,向折线图和饼图进军。