# 第 1 章 数据

> **本讲义配套集成教学页面**：[`./index.html`](./index.html)（讲解 ⇄ 代码 ⇄ 演示 三 tab 自由切换）
>
> 推荐教学用法：浏览器打开 lesson 页面 → 左侧选章节 → 三个 tab 来回切（讲解看原理、代码看 ⭐ 关键点、演示看实际效果）。

## 📌 教学目标

1. 理解数据的本质（DIKW 金字塔）与可视化在其中的位置
2. 掌握 Stevens 测量尺度 × Mackinlay 视觉通道的匹配原则
3. 熟练使用 D3 数据加载与转换 API
4. 理解 D3 Selection 的 enter / update / exit 三态模型
5. **重点掌握**：⭐ 标注的 JavaScript 关键编程点（13 个）

---

## 1.1 数据生成

### 你的原案例

本章采用中国石油大学（华东）2025 级新生数据。数据由 DeepSeek 大模型合成，含 80 条记录：

```
序号、姓名、性别、生日、年龄、班级、籍贯、民族
```

### 嵌入理论：AI 合成数据的可信度边界

| 维度 | 含义 | DeepSeek 生成数据 |
|---|---|---|
| 准确性 | 反映真实世界的程度 | ⚠️ 姓名/籍贯虚构 |
| 完整性 | 必填字段是否齐全 | ✅ 字段齐全 |
| 一致性 | 同一概念在不同地方表达一致 | ✅ 字段一致 |
| 时效性 | 数据反映当前状态 | ✅ 入学年份匹配 |

**AI 合成数据的"采样偏差"**：模型倾向"看起来合理"的分布（民族偏汉族、籍贯偏大城市）。

### ⭐ 关键代码段 ①：字段名映射

```javascript
// ⭐ 关键点 ①：字段映射 —— 把中文 CSV 列名转英文 JS 变量
const COLUMN_MAP = {
  "序号": "id",
  "姓名": "name",
  "性别": "gender",
  "生日": "birthday",
  "年龄": "age",
  "班级": "class",
};
```

**为什么不用中文 key？**
- JS 引擎对中文标识符的容错性差（老浏览器报错）
- 后端、JSON、URL 编码都可能引入乱码
- IDE 自动补全、搜索、重构工具对中文支持不一致

**最佳实践**：CSV 用人可读的中文名，JS 内部统一英文变量。

### ⭐ 关键代码段 ②：类型转换函数

```javascript
// ⭐ 关键点 ②：类型转换 —— 用户原文「推荐方式」
function transformRow(d) {
  return {
    id:       +d["序号"],              // 一元 + 把字符串转 Number
    name:     d["姓名"].trim(),       // trim() 去掉首尾空白
    birthday: new Date(d["生日"]),   // 字符串 → Date 对象
    age:      +d["年龄"],
    class:    d["班级"].trim(),
  };
}
```

**关键点解释**：
- `+d["年龄"]` 是一元加运算符，是 JS 里把字符串转数字的最快写法
- `new Date(str)` 解析 ISO 格式日期，对 `"2007/8/15"` 兼容性差，要用 `d3.timeParse("%Y-%m-%d")` 兜底
- **坑**：没有 `trim()` 的话，"软件工程1班 " 里的空格会让后续过滤/聚合出错

### ⭐ 关键代码段 ③：d3.csv 第二参直接传 row 回调

```javascript
// ⭐ 关键点 ③：一行完成加载 + 转换
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()`"省内存
- **异步加载的意义**：不阻塞页面渲染，用户能继续交互

### 配套 Demo

[`./index.html#ch-1-1`](./index.html) · 切到「代码」tab 看 ⭐ 关键点，切到「演示」tab 看实际加载效果。

---

## 1.2 数据定义

### 你的原案例（DIKW 金字塔）

| 层 | 原文 | 回答的问题 |
|---|---|---|
| 数据 Data | 原始、离散的观察结果 | — |
| 信息 Information | 经过处理、组织、赋予上下文的数据 | 谁/什么/何时/何地 |
| 知识 Knowledge | 对信息的进一步提炼和理解 | 如何 |
| 智慧 Wisdom | 运用知识做出明智判断和战略决策 | 为什么/应该怎么做 |

### 嵌入理论：可视化在 DIKW 中的位置

**关键洞见**：可视化主要把 **Data 升到 Information**，少数场景能升到 **Knowledge**，但难以跨越到 Wisdom。

- ✅ Data → Information：把表格"翻译"成可读图形（折线图、柱状图、热图）
- ⚠ Information → Knowledge：模式发现需要统计模型辅助（机器学习）
- ❌ Knowledge → Wisdom：决策与战略超出可视化能力

**Rowley (2007) 批评**：DIKW 是过度简化的层次模型。但作为教学脚手架仍然最有价值。

### ⭐ 关键代码段 ①：SVG 金字塔路径

```javascript
// ⭐ 关键点 ①：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.symbol()`（内置 ★●▲），自己 path 容易出错

### ⭐ 关键代码段 ②：交互高亮（事件）

```javascript
// ⭐ 关键点 ②：选择集绑定事件 —— 注意 function 而非箭头函数
const g = svg.append("g")
  .style("cursor", "pointer")
  .on("mouseover", function () {       // ⚠️ 必须 function，不能箭头函数
    d3.select(this).select("path")      //    因为要用 this 访问当前元素
       .transition().duration(150)
       .attr("fill", d3.color(layer.color).brighter(0.5));
  })
  .on("click", () => showDetail(layer));  // 这里箭头函数 OK（不用 this）
```

**关键点解释**：
- **必须用 `function()` 而非箭头函数**——因为 D3 回调要用 `this` 访问当前元素，箭头函数捕获外层 `this` 会失效
- 这是 D3 最常见的坑
- `transition().duration()` 是 D3 内置动画 API，比 jQuery 还简洁

### 配套 Demo

[`./index.html#ch-1-2`](./index.html) · 演示 tab 可点击金字塔各层查看详情。

---

## 1.3 数据属性

### 你的原案例（3 大类）

| 类别 | 例子 | 说明 |
|---|---|---|
| 类别型 | 姓名、性别 | 无顺序关系 |
| 序数型 | 班级、学位 | 有顺序，无间距 |
| 数值型 | 年龄、考试成绩 | 定量 |

### 嵌入理论：Stevens 测量尺度（1946）—— 4 类

比你的原文更精细的 4 类：

| 类别 | 允许的运算 | 典型示例 | 对应你原文 |
|---|---|---|---|
| **名义 Nominal** | 等价 / 不等价 | 姓名、性别 | 类别型 |
| **序数 Ordinal** | + > < | 等级、班级序号 | 序数型 |
| **区间 Interval** | + 加减 | 温度（°C） | — |
| **比率 Ratio** | + 加减乘除 | 年龄、价格 | 数值型 |

**关键区别**：区间尺度没有"真零点"（0°C 不是"没有温度"）；比率尺度有真零点（0 元 = 没有钱）。

### 嵌入理论：Mackinlay 视觉通道排序（1986）

| 档次 | 通道 | 适合的属性 |
|---|---|---|
| **定量型** | 位置、长度、角度 | 比率 / 区间 |
| **顺序型** | 亮度、密度、饱和度 | 序数 |
| **定性型** | 色相、形状 | 名义 |

**核心发现**：位置是最强的定量通道；色相是最强的定性通道。**错配 → 信息丢失**。

### ⭐ 关键代码段 ①：比例尺选型

```javascript
// ⭐ 关键点 ①：scale 选型 —— 属性 ↔ 通道的映射
// 比率型数据 → scaleLinear（连续定量）
const x = d3.scaleLinear().domain([0, 100]).range([0, 800]);

// 名义/序数型数据 → scaleBand（离散分类）
const x = d3.scaleBand()
  .domain(["软件1班", "软件2班", "数据1班"])
  .range([0, 800]);
```

**关键点解释**：
- `domain()` 是数据域（输入），`range()` 是值域（输出像素）
- **选错比例尺是初学者最常犯的错**：类别数据用 scaleLinear 会算出没有意义的中间值（如 0.5 班）
- **记忆口诀**：连续定量用 Linear，离散分类用 Band，序数可用 Point

### ⭐ 关键代码段 ②：colormap 选型

```javascript
// ⭐ 关键点 ②：颜色映射 —— 顺序 vs 定性 vs 发散
// 顺序型（顺序有意义）→ Viridis / Blues —— CVD 友好
const color = d3.scaleSequential(d3.interpolateViridis);

// 定性型（无顺序）→ schemeTableau10
const color = d3.scaleOrdinal().range(d3.schemeTableau10);

// 发散型（有中点）→ RdBu
const color = d3.scaleDiverging(d3.interpolateRdBu);
```

**关键点解释**：
- Viridis 是顺序 colormap 标杆，色觉缺陷（CVD）友好且感知均匀
- **常见错误**：用彩虹色 `interpolateRainbow` 编码数值——既不 CVD 友好，又有假边界
- **什么时候用发散型**：有"中间值"的数据（正负、温度 vs 平均值）

### 配套 Demo

[`./index.html#ch-1-3`](./index.html) · 4 个 sub-demo 对比演示。

---

## 1.4 数据变换

### 你的原案例（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 天然并行）、可缓存（纯函数可记忆化）。

**OLAP 数据立方体雏形**：`transpose` 做宽/长格式转换，`group + rollup` 做切片/切块/钻取。

### ⭐ 关键代码段 ①：map / reduce 链式组合

```javascript
// ⭐ 关键点 ①：函数式数据流 —— 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（笛卡尔积）

```javascript
// ⭐ 关键点 ②：cross —— 笛卡尔积
const genders = ["男", "女"];
const majors  = ["软件工程", "数据科学"];
const combos  = d3.cross(genders, majors);
// → [["男","软件工程"], ["男","数据科学"], ["女","软件工程"], ["女","数据科学"]]
```

**关键点解释**：
- 笛卡尔积常用在"两两组合"分析（如性别 × 专业的人数统计）
- 第二参 reducer 可选，用来"合并"两个元素
- **扩展**：n 个数组的笛卡尔积大小是各数组长度的乘积

### ⭐ 关键代码段 ③：group / rollup（聚合）

```javascript
// ⭐ 关键点 ③：group 与 rollup —— 层次聚合
// group 返回 Map（保留所有元素）
const byClass = d3.group(students, s => s.class);

// rollup 返回 Map（每个 key 只保留聚合值）
const countByClass = d3.rollup(
  students,
  v => v.length,         // reducer
  s => s.class           // key 提取函数
);
```

**关键点解释**：
- `group` vs `rollup` 是"分组保留"vs"分组聚合"的区别
- `rollup` 第二参是 reducer 函数（这里取长度）
- 后续章节 doc-02 会详细用 rollup 做树形数据

### 配套 Demo

[`./index.html#ch-1-4`](./index.html) · 演示 tab 下拉切换 10 个函数（含 group / rollup 加项）。

---

## 1.5 D3 数据选择器

### 你的原案例（6 个方法）

| 方法 | 返回 |
|---|---|
| `d3.select` | 单个元素 |
| `d3.selectAll` | 多个元素的 selection |
| `selection.select` | 每个选中元素的后代中第一个 |
| `selection.selectAll` | 后代中所有 |
| `selection.selectChild` | 直接子元素第一个 |
| `selection.selectChildren` | 所有直接子元素 |

### 嵌入理论：Data-Join 三态模型

D3 的核心是 **Data-Join**：把数据数组"绑定"到 DOM 元素上。当数据变化时：

- 新数据 > 旧数据：多出来的进入 **enter**（要 append）
- 新数据 = 旧数据：原有元素进入 **update**（要改属性）
- 新数据 < 旧数据：多余的元素进入 **exit**（要 remove）

### ⭐ 关键代码段 ①：.join(enter, update, exit) 三态

```javascript
// ⭐ 关键点 ①：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 函数保身份

```javascript
// ⭐ 关键点 ②：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}` 完全相同的思想

### ⭐ 关键代码段 ③：select vs selectAll

```javascript
// ⭐ 关键点 ③：选择器的核心区别
// select：返回 1 个元素（第一个匹配）；数据绑定只对这一个元素
const title = d3.select("#title");

// selectAll：返回多个元素；数据绑定按顺序一一对应
const bars = d3.selectAll(".bar").data(values);
```

**关键点解释**：
- `select` 用于"操作单个元素"，`selectAll` 用于"批量绑定数据"
- **常见错误**：用 `selectAll(".bar")` 给数组绑定数据时，元素数量与数据数量必须匹配，否则"留空"或"溢出"
- **最佳实践**：先用 `selectAll` 创建占位元素（可能为空），再 `.data().join(...)`

### 配套 Demo

[`./index.html#ch-1-5`](./index.html) · 演示 tab 点击按钮触发 enter / update / exit。

---

## 📚 教师视角 · 本章教学要点

### 13 个 ⭐ 关键点速查

| 章节 | 关键点 |
|---|---|
| 3.1 | ① 字段名映射 ② 类型转换 ③ d3.csv 第二参回调 |
| 3.2 | ① SVG path ② **function vs 箭头函数** |
| 3.3 | ① scale 选型 ② colormap 选型 |
| 3.4 | ① 链式组合 ② cross 笛卡尔积 ③ group/rollup |
| 3.5 | ① join 三态 ② key 函数 ③ select vs selectAll |

### 重点 & 难点

**重点**：Data-Join 三态、Stevens × Mackinlay 对应、字段映射+类型转换。

**难点**：
- key 函数的意义（学生最容易忽略）
- `function` vs 箭头函数（D3 回调的 this 绑定）
- colormap 顺序/定性/发散选型

### 考察点

1. 给一段数据，判断应该用什么视觉通道
2. 给出错误 demo，让学生指出问题
3. 让学生解释"为什么用 key 函数"
4. 让学生手写一个 group + rollup 聚合

---

## 🏃 全部 demo 一键运行

### 集成教学页面（推荐）

```bash
cd docs/doc-01-数据/lesson
python -m http.server 8080
# 浏览器打开 http://localhost:8080
```

**特性**：讲解 ⇄ 代码 ⇄ 演示 三 tab 自由切换，13 个 ⭐ 关键点都有文字解释。

### 独立 demo（备选）

```bash
cd docs/doc-01-数据/demo
python -m http.server 8081
```

5 个 demo 各自独立，适合课后练习。