第 1 章 数据

从数据生成到 Data Binding · 5 个小节

Alt + ← / → 切换章节课件

1.1 数据生成

基于新生数据(DeepSeek 合成)讲解异步加载与字段映射

你的原案例

本章采用中国石油大学(华东)2025 级新生数据,阐述数据的特性。为了防止数据隐私泄露,数据采用 DeepSeek 大模型生成。数据文件 data/newstudent.csv,含 80 条记录。

嵌入理论:AI 合成数据的可信度边界

数据质量四维度(Wang & Strong 1996):准确性 / 完整性 / 一致性 / 时效性。

DeepSeek 生成数据:

  • ✅ 完整性:字段齐全
  • ⚠️ 准确性:姓名/籍贯是虚构的,不能用作真实人口分析
  • ⚠️ 采样偏差:模型倾向"看起来合理"的分布(如民族偏汉族、籍贯偏大城市)

课堂要点:教学场景使用合成数据没问题,但必须标注"合成数据",避免学生误以为真实分布。

关键代码 ① 字段名映射

中文列名映射成英文变量名 —— 避免 JS 中带中文 key 引发语法/编码隐患。

⭐ // 关键点 ①:字段映射 —— 中文 → 英文
const COLUMN_MAP = {
  "序号": "id",
  "姓名": "name",
  "性别": "gender",
  "生日": "birthday",
  "年龄": "age",
  "班级": "class",
};

关键点解释:为什么不用中文 key? (1) JS 引擎对中文标识符的容错性差(老浏览器报错);(2) 后端、JSON、URL 编码都可能引入乱码; (3) IDE 自动补全、搜索、重构工具对中文支持不一致。 最佳实践:CSV 用人可读的中文名,JS 内部统一英文变量。

关键代码 ② 类型转换函数

CSV 加载后所有字段都是字符串,需要把序号/年龄 → Number,生日 → Date 对象。

⭐ // 关键点 ②:类型转换函数 —— 用户原文「推荐方式」
function transformRow(d) {
  return {
    id:       +d["序号"],              // 一元 + 把字符串转 Number
    name:     d["姓名"].trim(),       // trim() 去掉首尾空白
    birthday: new Date(d["生日"]), // 字符串 → Date 对象
    age:      +d["年龄"],
    class:    d["班级"].trim(),
  };
}

关键点解释: +d["年龄"] 是一元加运算符,是 JS 里把字符串转数字的最快写法(比 Number() / parseInt() 都短); new Date(str) 能解析 "2007-08-15" 这类 ISO 格式,但对 "2007/8/15" 兼容性差,要用 d3.timeParse("%Y-%m-%d") 兜底。 坑:没有 trim() 的话,"软件工程1班 " 里的空格会让后续过滤/聚合出错。

关键代码 ③ d3.csv 第二参直接传 row 回调

⭐ // 关键点 ③:一行完成加载 + 转换(用户原文「代码示例 1」)
const data = await d3.csv("data/newstudent.csv", transformRow);

// 验证类型确实转换了:
console.log(typeof data[0].age);        // "number"
console.log(data[0].birthday instanceof Date); // true

关键点解释:d3.csv 是异步函数,必须 await。 第二参的 row 回调会在每一行解析后立即调用——比先 await d3.csv(...) 再 .map(transformRow) 更省内存(边读边转)。 异步加载的意义:不阻塞页面渲染,用户能继续交互。

点击「重新加载」查看 CSV 异步加载、字段映射、类型转换 → 输出转换后表格 + 控制台日志。

1.2 数据定义

DIKW 金字塔 —— 数据 → 信息 → 知识 → 智慧

你的原案例

DI 金 W 金字塔:数据是"原始原始数据",信息是"组织好的数据",知识是"模式/规律",智慧是"判断/战略"。

层原文回答的问题
数据原始、离散的观察结果—
信息经过处理、组织、赋予上下文的数据谁 / 什么 / 何时 / 何地
知识对信息的进一步提炼和理解如何
智慧运用知识做出明智判断和战略决策为什么 / 应该怎么做

嵌入理论:可视化在 DIKW 中的位置

关键洞见:可视化主要把 Data 升到 Information,少数场景能升到 Knowledge,但难以跨越到 Wisdom。

  • ✅ Data → Information:把表格"翻译"成可读图形(折线图、柱状图、热图)
  • ⚠️ Information → Knowledge:模式发现需要统计模型辅助(机器学习)
  • ❌ Knowledge → Wisdom:决策与战略超出可视化能力

Rowley (2007) 批评:DIKW 是过度简化的层次模型,现实中 Data 与 Information 的界限模糊。但作为教学脚手架,仍然最有价值。

关键代码 ① SVG 金字塔路径

金字塔由 4 个 trapezoid 拼接而成 —— D3 path 的 d 属性。

⭐ // 关键点 ①:trapezoid 路径 —— 从上到下 4 个点的连线
const topW = 60, botW = 120;
const yTop = 20, yBot = 100;
const path = `
  M ${cx - topW/2} ${yTop}
  L ${cx + topW/2} ${yTop}
  L ${cx + botW/2} ${yBot}
  L ${cx - botW/2} ${yBot}
  Z
`;

关键点解释:SVG path 的 M(moveto)、L(lineto)、Z(close) 是 3 个最常用指令。 模板字符串 `${...}` 可以直接嵌入 JS 变量,比字符串拼接更可读。 实践:复杂形状用 D3 的 d3.symbol()(内置 ★●▲ 等),自己 path 容易出错。

关键代码 ② 交互高亮(hover/click)

⭐ // 关键点 ②:选择集绑定事件 —— D3 的事件模型
const g = svg.append("g")
  .style("cursor", "pointer")
  .on("mouseover", function() {       // 注意 function 而非箭头函数
    d3.select(this).select("path")
       .transition().duration(150)
       .attr("fill", d3.color(layer.color).brighter(0.5));
  })
  .on("click", () => showDetail(layer));

关键点解释:必须用 function() 而非箭头函数—— 因为 D3 回调里要用 this 访问当前元素,箭头函数会捕获外层 this 导致失效。这是 D3 最常见的坑。 transition().duration() 是 D3 内置动画 API,比 jQuery 还简洁。

鼠标悬停金字塔 → 高亮该层 · 点击 → 右侧显示该层定义 / 示例 / 常用可视化

1.3 数据属性

Stevens 测量尺度 × Mackinlay 视觉通道排序

你的原案例(3 大类)

你的原文给出 3 类属性:

  • 类别型属性:符号或名称,无顺序关系(性别、姓名)
  • 序数型属性:属性值有顺序(学位:本科/硕士/博士)
  • 数值型属性:定量方法(年龄、价格)

嵌入理论:Stevens 测量尺度(1946)—— 4 类

Stevens 把测量尺度分为 4 类,比你的原文更精细:

类别允许的运算典型示例对应你原文
名义 Nominal等价 / 不等价姓名、性别类别型
序数 Ordinal+ > <等级、班级序号序数型
区间 Interval+ 加减温度(°C)—
比率 Ratio+ 加减乘除年龄、价格数值型

关键区别:区间尺度没有"真零点"(0°C 不是"没有温度");比率尺度有真零点(0 元 = 没有钱)。

嵌入理论:Mackinlay 视觉通道排序(1986)

Mackinlay 提出视觉通道分 3 档:

档次通道适合的属性
定量型位置、长度、角度比率 / 区间
顺序型亮度、密度、饱和度序数
定性型色相、形状名义

核心发现:位置是最强的定量通道;色相是最强的定性通道。错配 → 信息丢失。

关键代码 ① 比例尺选型

⭐ // 关键点 ①:scale 选型 —— 属性 ↔ 通道的映射
// 比率型数据 → scaleLinear(连续定量)
const x = d3.scaleLinear().domain([0, 100]).range([0, 800]);

// 名义/序数型数据 → scaleBand(离散分类)
const x = d3.scaleBand().domain(["软件1班", "软件2班"]).range([0, 800]);

关键点解释:D3 比例尺的 domain() 是数据域(输入),range() 是值域(输出像素)。 选错比例尺是初学者最常犯的错:类别数据用 scaleLinear 会算出没有意义的中间值(如 0.5 班)。 记忆口诀:连续定量用 Linear,离散分类用 Band,序数可用 Point。

关键代码 ② colormap 选型

⭐ // 关键点 ②:颜色映射 —— 顺序型 vs 定性型 vs 发散型
// 顺序型(顺序有意义)→ interpolateViridis / interpolateBlues
const color = d3.scaleSequential(d3.interpolateViridis);

// 定性型(无顺序)→ schemeTableau10
const color = d3.scaleOrdinal().range(d3.schemeTableau10);

// 发散型(有中点)→ interpolateRdBu
const color = d3.scaleDiverging(d3.interpolateRdBu);

关键点解释:Viridis 是顺序 colormap 的标杆,色觉缺陷(CVD)友好且感知均匀。 常见错误:用彩虹色 interpolateRainbow 编码数值数据 —— 既不 CVD 友好,又有假边界(蓝色→绿色跳跃)。 课堂提问:什么时候用发散型?答:有"中间值"的数据(如正负、温度 vs 平均值)。

4 个 sub-demo:① 类别用色相(✓)② 序数用亮度(✓)③ 数值用位置(✓)④ ❌ 数值用色相(错配,看不出大小关系)

1.4 数据变换

D3 8 个函数式数组操作

你的原案例(8 个函数)

你的原文列出 D3 提供的 8 个常见函数:

  1. map(iterable, mapper) —— 类似 Array.map
  2. reduce(iterable, reducer, init) —— 类似 Array.reduce
  3. cross(...iterables, reducer) —— 笛卡尔积
  4. merge(iterables) —— 合并为扁平数组
  5. pairs(iterable, reducer) —— 相邻元素对
  6. transpose(matrix) —— 矩阵转置
  7. zip(...arrays) —— 按位合并
  8. filter(iterable, test) —— 类似 Array.filter

嵌入理论:函数式数据流

这 8 个函数的共同特点:

  • ✅ 不可变(immutable):输入不变,返回新数组
  • ✅ 纯函数:相同输入 → 相同输出,无副作用
  • ✅ 可链式:返回值是数组,可以接下一层变换

为什么函数式?

  • 可推理:每层变换独立可测
  • 可并行:map/reduce/filter 天然支持并行化
  • 可缓存:纯函数的输出可以记忆化

数据立方体(OLAP)雏形:transpose 做宽/长格式转换,group + rollup 做切片/切块/钻取 —— 这正是 BI 系统的核心。

关键代码 ① map / reduce 的链式组合

⭐ // 关键点 ①:函数式数据流 —— map 后接 filter 再 reduce
const avgAge =
  students
    .filter(s => s.gender === "男")    // ① 过滤男生
    .map(s => s.age)                    // ② 提取年龄
    .reduce((sum, a) => sum + a, 0) // ③ 合计
  / students.filter(s => s.gender === "男").length; // ④ 求平均

关键点解释:链式调用的每一步返回新数组,不会修改原数组。 与命令式对比:命令式需要 4 个临时变量 + 一个 for 循环,函数式一行搞定。 性能权衡:每次 map/filter 都会创建新数组,对超大数组有性能损耗(百万级以上才显著)。

关键代码 ② cross(笛卡尔积)

⭐ // 关键点 ②:cross —— 笛卡尔积,用于两两组合
const genders = ["男", "女"];
const majors  = ["软件工程", "数据科学"];
const combos  = d3.cross(genders, majors);
// → [["男","软件工程"], ["男","数据科学"], ["女","软件工程"], ["女","数据科学"]]

关键点解释:笛卡尔积常用在"两两组合"分析(如性别 × 专业的人数统计)。 第二参 reducer 可选,用来"合并"两个元素。 扩展:3 个数组的笛卡尔积是 8 种,n 个数组的笛卡尔积是乘积大小。

关键代码 ③ group / rollup(聚合)

⭐ // 关键点 ③:group 与 rollup —— 层次聚合
// group 返回 Map(保留所有元素)
const byClass = d3.group(students, s => s.class);
// rollup 返回 Map(每个 key 只保留聚合值)
const countByClass = d3.rollup(
  students,
  v => v.length,
  s => s.class
);

关键点解释:group vs rollup 是"分组保留"vs"分组聚合"的区别。 rollup 第二参是 reducer 函数(这里取长度),后续章节 doc-02 会详细用 rollup 做树形数据。

下拉切换 8 个函数,右侧显示输入/输出对照 + 真实场景(新生数据)

1.5 D3 数据选择器

Selection 与 enter / update / exit 三态模型

你的原案例(6 个选择器方法)

你的原文梳理了 6 个选择器方法:

方法返回
d3.select单个元素
d3.selectAll多个元素的 selection
selection.select每个选中元素的后代中第一个
selection.selectAll后代中所有
selection.selectChild直接子元素第一个
selection.selectChildren所有直接子元素

嵌入理论:Data-Join 三态模型(Mike Bostock)

D3 的核心是 Data-Join:把数据数组"绑定"到 DOM 元素上。

当数据变化时:

  • 新数据 > 旧数据:多出来的进入 enter(要 append)
  • 新数据 = 旧数据:原有元素进入 update(要改属性)
  • 新数据 < 旧数据:多余的元素进入 exit(要 remove)
three states

Data-Join vs 命令式 DOM

命令式声明式(Data-Join)
createElement 后逐个 append.data(arr).join(...) 一行
全量重绘增量更新(性能好)
元素身份丢失key 函数保留稳定身份(动画自然)
与数据流脱节与函数式数据流天然契合

关键代码 ① .join(enter, update, exit) 三态

⭐ // 关键点 ①:D3 v5+ 的 join() API —— 优雅处理三态
const circles = svg.selectAll("circle")
  .data(currentData)
  .join(
    enter  => enter.append("circle").attr("fill", "green"),  // enter
    update => update.attr("fill", "cyan"),                              // update
    exit   => exit.remove()                                                   // exit
  );

关键点解释:join 的三个回调分别在 enter / update / exit 阶段被调用。 这种"声明式三态"比 v3 的 enter().append() + exit().remove() 写法更简洁,逻辑更不易出错。 类比:React 的 diff + patch —— 同样思想,但 React 帮你做了,D3 让你自己写变换函数。

关键代码 ② key 函数保身份

⭐ // 关键点 ②:key 函数 —— 元素身份的稳定器
// ❌ 不带 key:数据顺序变化 → 所有元素都"update",过渡动画错乱
svg.selectAll("circle").data(newData);

// ✅ 带 key:用唯一字段绑定身份,元素"知道自己是哪个"
svg.selectAll("circle").data(newData, d => d.id);

// 同样的逻辑用 React 描述:key={item.id}

关键点解释:key 函数是 D3 动画的灵魂。 没有 key 时,D3 按 index 绑定身份:数据重排会导致"所有元素都换位置"。 带 key 后,每个 circle 知道自己的 id 是什么,重排时只是 cx 变化,过渡动画自然。 类比:与 React 的 key={item.id} 完全相同的思想。

关键代码 ③ d3.select vs d3.selectAll

⭐ // 关键点 ③:选择器的核心区别
// select:返回 1 个元素(第一个匹配);数据绑定只对这一个元素
const title = d3.select("#title");

// selectAll:返回多个元素;数据绑定按顺序一一对应
const bars = d3.selectAll(".bar").data(values);

关键点解释:select 用于"操作单个元素",selectAll 用于"批量绑定数据"。 常见错误:用 selectAll(".bar") 给一个数组绑定数据时,元素数量与数据数量必须匹配,否则会"留空"或"溢出"。 最佳实践:先用 selectAll 创建占位元素(可能为空),再 .data().join(...)。

点击按钮触发 enter (绿) / update (蓝) / exit (红),观察三态切换