缩放与平移 — 几何缩放、程序缩放与语义缩放
缩放与平移 — 几何缩放、程序缩放与语义缩放
这一篇讲什么: 第 14 章的另一半。缩放看似简单(套个 transform 而已), 但原书用三个递进的层次拆开:基础(transform 挂对层、加约束)、 程序(按钮/回放,行为方法驱动)、语义(缩放不只是变大变小—— 轴要跟、线宽要稳、细节要分级出现)。这三层也是真实项目里逐个踩过去的坑序。
1. 顶层全景:一个对象 {x, y, k} 贯穿全篇
用户手势(双击/滚轮/捏合/拖拽) 你的代码(按钮/回放/联动)
│ │
└──────────► d3.zoom() 行为 ◄────────┘
│ on('zoom', evt)
▼
evt.transform = {x, y, k}
x,y:平移量 k:缩放倍率
│
┌───────────────┼───────────────────────┐
▼ ▼ ▼
挂到容器属性 重算比例尺(rescaleX/Y) 反算坐标(apply/invert)
(几何缩放) (轴跟着动的语义缩放) (tooltip/加点对齐)
图说:全部缩放语义浓缩成一个三元组;三种用法是它的三个消费方向。2. 基础缩放:挂对层、加约束
2.1 最小例子与一个坑
const zoom = d3.zoom().on('zoom', evt => {
d3.select("svg").attr("transform", evt.transform); // ← 坑在这
});
d3.select("svg").call(zoom);直接 transform 整个 <svg> 会让视口本身跟着缩放——内容越界、边框变形。
正解:行为 call 在 svg 上(它监听事件),transform 却施加给内部一个 <g> 数据层1:
svg.call(d3.zoom().on('zoom', evt =>
svg.select("g").attr("transform", evt.transform)));2.2 容器层级:数据区 ≠ SVG 区
带轴的图表里,轴要固定、只有绘图区缩放。原书的标准结构是四层:
<svg> ← 监听 zoom 行为
<g class="context"> ← 平移出边距
<svg class="chart"> ← 固定尺寸的「数据视口」(必须是 <svg> 才能设宽高)
<g class="container"> ← 真正吃 transform 的数据层数据层外包一层固定尺寸的内嵌 <svg>,等于天然「剪辑窗口」——点缩放后越界即被裁掉。
等效替代:不给内层 <svg>,改用 <clipPath> 裁数据层(原书两案并列,效果相同)2。
2.3 三 extent 与事件过滤
| 方法 | 约束什么 |
|---|---|
scaleExtent([0.5, 2]) | 缩放倍率范围(默认不限) |
translateExtent([[0,0],[w,h]]) | 平移边界(图角始终贴着视口) |
extent([[…],[…]]) | 缩放原点的参照区域,默认视口尺寸 |
事件分流也在这层:filter(evt => !evt.shiftKey) 让 Shift 场景让给刷选;
svg.on("dblclick.zoom", null) 直接拆掉某个内置监听(如双击缩放)3。
3. transform 对象:不可变的 {x, y, k}
事件对象里的 transform(也可用 d3.zoomTransform(node) 随时取)是个不可变对象:
{x, y, k} 默认恒等 {0, 0, 1}。塞进字符串上下文自动变
translate(x,y) scale(k)——所以 attr("transform", evt.transform) 一行就够4。
两个派生方法,顺序有讲究(先 translate 后 scale,反了平移量会被缩放)5:
const trsf1 = new d3.ZoomTransform(2, 25, 10); // {k:2, x:25, y:10}
const trsf2 = trsf1.translate(50,50).scale(1.5); // {k:3, x:75, y:60}
// 还有现成常量 d3.zoomIdentity = {0,0,1}别混淆:zoom.transform(行为的方法,往选择器上套变换)与 evt.transform
(变换数据对象)是两个东西,只是名字撞车——原书专门加了 Important note6。
4. 程序缩放:行为方法驱动
把「套变换」从事件里解放出来,就是按钮、回放、联动地图的玩法。
方法都走 selection.call(zoom.方法, …) 的形式7:
| 方法 | 语义 |
|---|---|
transform(sel, t) | 套上指定的变换对象(可配 transition 动画) |
translateBy(sel, dx, dy) | 相对平移 |
translateTo(sel, x, y) | 把视野中心对准 (x,y)——注意内容反向移动 |
scaleBy(sel, k) | 倍率相乘(受 scaleExtent 约束) |
scaleTo(sel, k) | 倍率设为绝对值(同样受约束) |
// 重置按钮:过渡回恒等变换
d3.select(".chart").transition().call(zoom.transform, d3.zoomIdentity);
// 动画序列:链式 call
d3.select(".chart").transition().delay(1000)
.call(zoom.transform, trsf1).transition().delay(1000)
.call(zoom.transform, trsf2)…
// 三连放大再缩回:scaleBy 累积 ×2 ×2 ×0.25
.call(zoom.scaleBy, 2).transition().call(zoom.scaleBy, 2).transition().call(zoom.scaleBy, .25)两个细节:transform() 方法无视 scaleExtent,scaleBy/scaleTo 遵守;
translateTo 移动的是「视野」而非「内容」——中心对准 (0,0) 等于内容往反方向走8。
程序缩放默认用 d3.interpolateZoom 平滑(平移中尺寸连续变化),
不想要可 interpolate(d3.interpolate) 换成线性9。
5. 语义缩放:缩放改变「看到的」,不只是「大小」
5.1 几何 vs 语义
container.attr("transform", evt.transform) 是几何缩放:一切跟着 k 变大变小——
散点的 1px 点变成大圆,地图的细国界变粗矩形。语义缩放则让图保持可读、并按层级揭示细节:
放大地图时出现更多城市,放大星图时出现更多星10。
5.2 三件工具
① 反缩放(保持视觉尺寸):一切想「不变粗」的量都除以 k
function update() {
d3.selectAll("circle").attr("r", 5/k).style("stroke-width", 1/k);
d3.selectAll("text").style("font-size", 10/k);
d3.selectAll("path.grid.major").style("stroke-width", 3/k);
}
.on('zoom', evt => { container.attr("transform", evt.transform); update(); });
② 重算比例尺(让轴跟着数据动)
d3.select(".x-axis").call(xAxis.scale(evt.transform.rescaleX(xScale)));
d3.select(".y-axis").call(yAxis.scale(evt.transform.rescaleY(yScale)));
// rescaleX 返回一个「domain 已按当前变换调整」的比例尺副本,原尺不动
③ 点级取舍(按 k 决定画不画、画多大)
function radius(d, k) {
if (d.n * k > 1) return 1/k + d.n * k/2000; // 大点缓慢变大
else if (d.n * k < 0.1) return 0; // 太小不画,防糊
else return d.n/k; // 中等点保持视觉大小
}①+③ 合起来的效果(原书 5 万点散点实验):缩放时点的屏幕尺寸稳定, 太密的点自动隐去,坐标轴数字随视野更新11。
5.3 性能:Canvas + requestAnimationFrame
5 万个圆点在 SVG 里做语义缩放会卡——DOM 太多。换成 Canvas 重绘后还有一个坑:
zoom 事件触发得比屏幕刷新快,画不过来。原书的提速只有一行:
requestAnimationFrame(() => paint())——把重绘对齐到浏览器的帧节奏12。
5.4 坐标换算:apply 与 invert
transform 还提供单点换算:apply([x,y]) 把数据点投到缩放后的屏幕;
invert([x,y]) 反向——缩放状态下要按点击位置加一个新点、或给悬停点摆 tooltip,
就得用它把屏幕坐标译回数据坐标13。
6. 作者的判断与证据
| 判断 | 性质 | 依据 |
|---|---|---|
「transform 挂内层 <g>,别挂 <svg>」 | 实操铁律 | 视口随内容变形的反例演示1 |
| 「语义缩放要除以 k」 | 核心技法 | 反缩放 update() 的最小实现11 |
| 「Canvas + rAF 是大数据缩放标配」 | 性能经验 | 5 万点实验的卡顿与修复12 |
7. 边界与局限
apply/invert正文明说「not covered」,只有仓库示例13。interpolateZoom的高级用法(视图间平滑飞行)同样只在 Extras。- 触摸双指捏合的数学(两指中点、距离比)由行为内部处理,不拆。
- 语义缩放的「细节层级」策略(何时出现城市名)只有示例没有普适配方。
8. 可带走的
- zoom 的全部状态 =
{x, y, k};行为挂外层、transform 施加内层数据<g>; - 三层容器(svg → context → 内层 svg/clipPath → 数据层)是带轴缩放的标准结构;
- scaleExtent 限倍率、translateExtent 限平移、filter 分流事件、
xxx.zoom拆内置监听; - transform 不可变:translate/scale 方法派生新对象,先 translate 后 scale;
- 程序缩放四方法:transform/translateBy/translateTo/scaleBy/scaleTo,走
.call(zoom.方法, …); - scaleBy 相对、scaleTo 绝对;translateTo 移动的是视野(内容反向);
- 语义缩放三件套:视觉尺寸 ÷k、rescaleX/Y 重算轴、按 k 分级取舍细节;
- 大数据语义缩放:Canvas 重绘 + requestAnimationFrame 对齐帧率;
- apply/invert 做缩放状态下的单点坐标换算(tooltip、加点)。
9. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 最小 zoom 与挂层之坑 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1024(搜「d3.zoom().on('zoom'」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1030(搜「keep SVG dimensions fixed」) |
| 配置方法与三 extent | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1050(搜「scaleExtent(extent)」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1078(搜「scaleExtent([0.5, 2])」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1084(搜「translateExtent」) |
| 容器层级与裁剪 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1105(搜「context」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1111(搜「chart = context.append("svg")」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1135(搜「clipPath」) |
| filter 与拆监听 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1162(搜「evt.type === 'wheel'」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1172(搜「dblclick.zoom」) |
| transform 对象与方法 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1179(搜「x, y, and k」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1194(搜「toString()」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1220(搜「new d3.ZoomTransform(2, 25, 10)」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1225(搜「trsf2」) |
| transform 名混淆警告 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1332(搜「Important note」) |
| 程序缩放方法表 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1288(搜「transform(selection,transform)」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1349(搜「scaleBy, 2」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1381(搜「translateBy, 200, 150」) |
| interpolateZoom | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1405(搜「interpolateZoom」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1410(搜「interpolate(d3.interpolate)」) |
| 语义缩放三件套 | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1424(搜「Semantic zoom」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1464(搜「5/k」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1484(搜「rescaleX(xScale)」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1522(搜「radius(d, k)」) |
| Canvas + rAF | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1556(搜「requestAnimationFrame」) |
| apply/invert | Events and Interactivity | text/21-ch14-chapter-14-events-and-interactivity.txt:1569(搜「apply([x,y])」) · text/21-ch14-chapter-14-events-and-interactivity.txt:1582(搜「not covered in this book」) |
Footnotes
-
出处:「Events and Interactivity」第 1016–1042 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1024,搜「d3.zoom().on('zoom'」;:1031,搜「keep SVG dimensions fixed」;:1033,搜「select("g").attr("transform"」)。 ↩ ↩2 -
出处:「Events and Interactivity」第 1094–1144 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1109,搜「const context = svg.append」;:1111,搜「chart = context.append("svg")」;:1114,搜「created with <svg> instead of <g>」;:1135,搜「clipPath」)。 ↩ -
出处:「Events and Interactivity」第 1044–1065、1158–1176 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1078,搜「scaleExtent([0.5, 2])」;:1162,搜「evt.type === 'wheel'」;:1172,搜「dblclick.zoom」)。 ↩ -
出处:「Events and Interactivity」第 1177–1235 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1179,搜「x, y, and k」;:1194,搜「toString()」;:1216,搜「immutable」)。 ↩ -
出处:「Events and Interactivity」第 1220–1228 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1220,搜「new d3.ZoomTransform(2, 25, 10)」;:1225,搜「trsf2」;:1227,搜「translate first, then scale」)。 ↩ -
出处:「Events and Interactivity」第 1332–1338 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1332,搜「Important note」)。 ↩ -
出处:「Events and Interactivity」第 1252–1402 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1288,搜「transform(selection,transform)」;:1307,搜「call(evt.target.transform, d3.zoomIdentity)」)。方法表照原书 Table 14.13 转写。 ↩ -
出处:「Events and Interactivity」第 1341–1399 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1372,搜「ignored by transform()」;:1385,搜「translateTo() doesn't translate the selection, but the view」)。 ↩ -
出处:「Events and Interactivity」第 1404–1413 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1405,搜「interpolateZoom」;:1410,搜「interpolate(d3.interpolate)」)。 ↩ -
出处:「Events and Interactivity」第 1424–1438 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1429,搜「geometric zooming」;:1431,搜「semantic zooming」)。 ↩ -
出处:「Events and Interactivity」第 1441–1550 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1464,搜「5/k」;:1483,搜「rescaleX(xScale)」;:1522,搜「radius(d, k)」)。 ↩ ↩2 -
出处:「Events and Interactivity」第 1551–1560 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1556,搜「requestAnimationFrame」)。 ↩ ↩2 -
出处:「Events and Interactivity」第 1562–1586 段(
text/21-ch14-chapter-14-events-and-interactivity.txt:1569,搜「apply([x,y])」;:1582,搜「not covered in this book」)。 ↩ ↩2