第 10 章 Plot 如何站在 D3 肩膀上数据可视化库 Observable Plot 的官方定位是 “a D3-based library”。这句话很容易被误读成两种极端要么以为它只是 D3 的语法糖要么以为它和 D3 没什么关系。本章用实测数据把这两种印象都修正掉Plot 确实深度依赖D352 个文件、243 个符号但它把自己造的七套机制都放在了 D3 没有覆盖的地方——比例尺编排、变换管线、分面状态、样式体系、依赖注入、警告系统、惰性模板。读完本章你应该能回答Plot 一共从 D3 借用了多少东西集中在哪几个 D3 子模块哪些 D3 能力是 Plot刻意不用的这个不用透露了什么设计立场如果你要基于 D3 写一个类似的库可以照搬哪几条经验10.1 先量化52 个文件、60 条 import、243 个符号不要凭印象讨论依赖深浅直接测。下面这条 PowerShell 脚本统计src/下所有.js文件从d3导入的符号$filesGet-ChildItemsrc-Recurse-Filter*.js$symNew-ObjectSystem.Collections.Generic.HashSet[string]$stmts 0$fileSetNew-ObjectSystem.Collections.Generic.HashSet[string]foreach($fin$files){$tGet-Content-Raw-Encoding UTF8$f.FullName$ms[regex]::Matches($t,(?s)import\s*\{([^}]*?)\}\s*from\s*d3)if($ms.Count-gt0){[void]$fileSet.Add($f.Name)}foreach($min$ms){$stmtsforeach($xin($m.Groups[1].Value-split,)){$k($x-replace\sas\s.*,).Trim()if($k){[void]$sym.Add($k)}}}}files importing d3:$($fileSet.Count)import statements:$stmtsdistinct d3 symbols:$($sym.Count)实测结果v0.6.17files importing d3: 52 import statements: 60 distinct d3 symbols: 243三个数字各有含义数字含义52 / 81约64% 的源文件直接依赖 D3——D3 是真正的基础设施不是可选项60 条 import平均每个文件 1.15 条说明导入很集中多数文件只从 D3 取 1~3 个函数243 个符号覆盖面极广从scaleLinear到contourDensity到randomLcg一个有意思的写法细节Plot 全部从伞包d3导入而不是从d3-scale/d3-array这样的子包。这样做的代价是必须靠 tree-shaking 才能去掉没用到的模块收益是依赖管理极简——只有一个版本号要对齐。对于一个要被大量下游项目引用的库来说这个取舍是合理的。10.2 Plot 从 D3 借用了哪九个领域把 243 个符号按 D3 子模块归类可以清楚看到 Plot 的能力来源D3 子模块Plot 用它做什么典型符号代表文件d3-scale比例尺的底层实现scaleLinearscaleBandscaleOrdinalscaleUtcscaleDivergingscaleQuantilescaleThresholdscaleImplicittickstickIncrementscales/*.jsd3-scale-chromatic配色方案的色表来源interpolateTurbointerpolateViridisschemeObservable10schemeTableau10schemeRdBu…scales/schemes.jsd3-array数据聚合的原语minmaxsummeanmedianmodedeviationvariancequantilegrouprollupcrossrangebisecttransforms/group.js、facet.jsd3-selectionDOM 操作与命名空间selectcreatornamespacespointerplot.js、context.js、style.jsd3-shape几何路径与符号lineareasymbolCirclesymbolSquarecurveLinearcurveMonotoneXmarks/line.js、marks/area.js、symbol.jsd3-format数字与时间格式化formatformat.js、marks/axis.jsd3-time / d3-time-format时间刻度与解析timeDaytimeMonthutcYearutcFormattimeFormatmarks/axis.js、scales/temporal.jsd3-geo / d3-contour / d3-delaunay / d3-hierarchy领域专用算法geoPathgeoAlberscontourscontourDensityDelaunayclusterstratifyprojection.js、marks/contour.js、transforms/hexbin.js、transforms/tree.jsd3-interpolate / d3-random数值插值与可控随机interpolateRgbinterpolateNumberpiecewiserandomLcgscales/quantitative.js、marks/...这张表给出的第一条经验是把数学与算法交给 D3把约定与编排留给自己。Plot 从 D3 拿走的几乎全是原语scale 构造函数、数组聚合、路径生成器而不是流程D3 也没有流程可给。10.3 同一个任务D3 怎么做 vs Plot 做了什么抽象的比较不如三个具体例子。10.3.1 例一比例尺——从手动组装到自动推断在纯 D3 里一张点图的横轴至少要写六行而且每一行都要你手算// 纯 D3所有决定都由人来做constxd3.scaleLinear().domain([d3.min(data,dd.weight),d3.max(data,dd.weight)])// ① 值域.nice()// ② 圆整.range([40,width-20]);// ③ 像素范围自己定的边距constyd3.scaleLinear().domain(d3.extent(data,dd.height)).range([height-30,20]);// ④ 还得记得反向在 Plot 里同样的语义只需要Plot.dot(data,{x:weight,y:height})少掉的四行并没有消失而是变成了第 6 章那套规则inferScaleType判断类型、inferDomain算值域、autoScaleRange从dimensions反推像素范围、autoScaleRangeY负责反向。Plot 的价值不在于替你做决定而在于把决定变成了有次序、可覆盖的默认值// 任何一层都可以被单独接管其它层仍然自动Plot.dot(data,{x:{value:weight,domain:[0,100]},y:height})10.3.2 例二坐标轴——D3 的axisBottomvs Plot 的 axis markD3 画轴的标准姿势是svg.append(g).attr(transform,translate(0,${height-marginBottom})).call(d3.axisBottom(x).ticks(5));// 立刻输出一堆 DOM它有三个特点立即求值调用即产生 DOM、依赖外部 scale必须先有 x、位置要手写translate。Plot 的坐标轴则是一个markmarks/axis.js走的是与其他图形完全相同的生命周期// 隐式插入plot.js L578-L587后轴会经历与 Dot 一样的流程// initialize → (initializer 生成 tick 通道) → scale → render这样做换来三件事每件都是 D3 的axisBottom结构上做不到的轴可以参与分面锚点体系第 9 章facetAnchor: left-empty之类的行为需要在mark 层做判断而不是在D3 axis 层。轴可以和其他 mark 共享比例尺与样式体系第 8 章的三层样式。轴的刻度ticks可以延迟到 initializer 阶段生成——因为轴需要知道最终的 dimensions 才能决定显示几个刻度而这正好是第 5 章步骤 14 的用途。但 Plot 并没有教条式地拒绝 D3src/legends/ramp.js里就老老实实用了axisBottom来画色带下面的刻度。判断标准是这一层需不需要参与 Plot 的管线——图例的色带是独立的、一次性的 SVG直接借用 D3 更划算。10.3.3 例三数据绑定——D3 的 join vs Plot 的 indexD3 v3 时代需要写enter()/update()/exit()三段式v5 之后有selection.join()。Plot 走了一条更原始的路把 index 数组当数据绑定给 D3 selection第 8 章。// Plotmarks/dot.js L88-L91精简g.selectAll().data(index)// 绑定的是 [0, 3, 4, 7] 这样的下标.enter().append(circle?circle:path)这样做的原因是 Plot 不需要更新语义它每次都重新渲染整张图不可变、无状态。既然没有 update/exitindex这种最轻的绑定方式就足够还顺带得到了两个好处节点池pool选项可以安全复用因为绑定的是数字而不是对象引用复用节点不会引发数据错配属性写入可以走常量优先的快速路径R ? (i) R[i] : r这种双形态让 d3 能跳过函数调用第 8 章 8.2 节。结论D3 的 selection 是一套通用 DOM 编程抽象Plot 只取其中创建元素 设置属性这一小块然后把什么时候创建、创建多少个完全掌握在自己手里。10.4 Plot 自己造的七套机制那么 D3 没有提供什么下面这七件事在 D3 里找不到对应物正是 Plot 的核心资产#Plot 自研机制位置为什么 D3 没有1比例尺系统编排scales.jsscales/*D3 只提供单个 scale 构造函数从 channels 推断整套 scale是应用层问题2声明式管线plot.jsD3 是命令式的没有一次声明、完整求值的概念3变换管线transforms/*17 个文件D3 只有group/rollup这样的数组原语没有以 mark 为单位的数据变换4分面状态机facet.jsD3 没有小倍数概念分面在 D3 里是写个循环手动摆5样式体系style.js478 行D3 的.attr()是逐个设值没有通道 → 属性的映射约定6依赖注入环境context.jsplot.js的 6 处挂载D3-selection 默认走全局document无法在 Node 里直接跑7弱约束与警告warnings.jsD3 的哲学是不做判断自然也不会有数据看起来像日期字符串这类提醒补充两个小而关键的自研模块template.js把模板字符串编译成惰性函数第 8 章 8.6 节避免字符串拼接后才发现用不上的开销interactions/pointer.js只封装d3.pointer把鼠标坐标 → 数据坐标这一步做成可复用组件。10.5 刻意不用的部分比用到的部分更能说明立场实测 243 个符号里完全找不到下面这些 D3 模块的痕迹未使用的 D3 模块通常用来做Plot 的替代选择d3-zoom/d3-brush/d3-drag平移、缩放、框选、拖拽交互完全不提供图是静态的 可选tip交互交给上层框架d3-transition动画与过渡不提供plot()每次输出全新 DOMd3-force力导向布局不提供Plot 不是图网络库d3-quadtree最近邻查询用interval-tree-1d区间查询适配dodge的重叠检测d3-chord/d3-sankey在 d3 之外弦图、桑基图由用户用 mark 组合实现d3-axis直接输出坐标轴 DOM自己在marks/axis.js里画唯一例外legends/ramp.js用了axisBottom不提供交互与动画是一个极其重要的定位决策它带来三个连锁结果plot()可以是纯函数输入 options输出一个全新的 DOM 节点没有内部可变状态、没有事件监听泄漏。这让它能安全地用在 React/Vue 的渲染函数里也可以在 Node/jsdom 里跑测试与静态导出。整张图是可哈希、可缓存的因为输出只依赖输入把它当成数据 → 图片的映射非常自然这也是 Plot 常被用于服务端出图的原因。交互成为组合层的事需要联动刷选、缩放时用Plot.plot()重画一张图而不是让库去 mutate DOM配合figure.scale()读取/改写 domain 即可。10.6 依赖清单也是架构一个可改进的细节最后看package.jsonv0.6.17dependencies:{d3:^7.9.0,interval-tree-1d:^1.0.0,isoformat:^0.2.0,rimraf:^6.1.3}四点观察只有 4 个运行时依赖而且都很小、很专一interval-tree-1d只被transforms/dodge.js用区间重叠检测isoformat只被format.js与options.js用ISO 8601 解析/格式化。依赖的粒度对齐到功能点是好设计。rimraf显然放错了位置它是一个删除文件的 CLI 工具在源码里没有任何 import实测 grep 0 命中scripts.prepublishOnly用的是rm -rf。它应该待在devDependencies。依赖清单的准确性也是代码质量的一部分——多一个依赖下游就多一份安装体积与安全审计负担。main: src/index.js源码即产物Plot不强制下游使用打包后的dist而是直接把 ESM 源码作为入口dist/plot.umd.min.js只服务 CDN 场景。这要求源码本身可被任意打包器处理——这也是它坚持只用 ESM import、不引入运行时语法糖的原因之一。sideEffects: [./src/index.js]向打包器声明其余模块无副作用从而允许整模块级别的 tree-shaking。一个 243 个 D3 符号的重依赖库能做到用什么打什么靠的就是这一行。10.7 三条可迁移的经验如果你要基于某个底层库D3、ECharts、Canvas API…写上层封装Plot 的取舍值得借鉴底层库给你原语你要自己造约定。D3 里没有默认比例尺这种东西因为它不知道你的领域但 Plot 知道统计图表的 x 轴通常不需要从 0 开始于是它把这类判断固化成规则。封装的真正价值是领域默认值而不是语法更短。默认值要可分层接管。Plot 的x: weight→x: {value: weight, domain: [...]}→x: {value: weight, domain: [...], type: log, scale: ...}是同一套语法的三种深度。使用者永远不需要全盘接管这才让默认值不会变成黑箱。对底层库保持按需取用而非阵营忠诚。Plot 没画坐标轴却用了axisBottom画图例色带不提供交互却在interactions/里封装pointer。判断依据永远是这一层是否属于我的管线而不是这个模块是不是 D3 的。10.8 常见误区「Plot 是 D3 的语法糖」——如果只是语法糖它不会自研 17 个变换、一个分面状态机、一套样式体系。它复用的是算法自研的是流程与约定。「用了 Plot 就不需要懂 D3」——日常画图确实不需要但一旦要自定义 mark第 11 章或自定义 scale第 12 章你会立刻遇到d3.scaleLinear、d3.symbol、context.path这些概念。Plot 是 D3 的上层不是替代品。「全部从d3伞包导入会导致包体积巨大」——源码层面确实导入了 243 个符号但配合sideEffects声明与 ESM打包器只保留真正用到的部分。依赖多与体积大不能划等号。「Plot 不提供交互是因为做不到」——是主动选择。不提供交互换来的是纯函数式渲染这直接决定了React 里安全使用与服务端出图两个能力。10.9 本章小结实测52/81 个源文件、60 条 import、243 个 D3 符号从伞包d3统一导入靠 tree-shaking 控制体积。Plot 从 D3 借的主要是算法原语scale 构造函数、数组聚合、路径/符号生成、格式化、地理与层次布局。三个任务上 D3 与 Plot 的分工比例尺手动组装 → 自动推断、坐标轴命令式 DOM → 参与管线的 mark、数据绑定join → index 常量优先。Plot 自研的七套机制里最能体现定位的是分面状态机、样式体系、依赖注入最能体现立场的是不提供 zoom/brush/drag/transition。设计原则可总结为三条造领域默认值、默认值可分层接管、对底层库按需取用。理论部分到此结束。
企业数字化 ERP 产品动态
相关推荐
从 TypeScript 到 C#:现代 SDK 移植的完整实战与踩坑记录 把 TypeScript 写的 SDK 原样搬到 C# 里,这事听着简单,做起来全是坑。尤其对象是 Codex SDK 这种带流式响应、事件回调、多环境配置的现代 SDK,不是把interface改成class、把Promise换成Task就完事的。我最近完整做了一遍这个移植,… · 2026/9/25 18:05:56
5G NR通感一体化ISAC系统级模拟器设计:从OFDM波形到距离多普勒处理 简介:基于5G NR的通信感知一体化(ISAC)系统级模拟器源码工程,面向通信工程、电子信息、人工智能等专业的本科毕业设计或课程设计场景,以Matlab仿真实例完整演示5G新空口框架下的综合传感与通信联合仿真流程与数据分析方… · 2026/9/25 18:05:56
一人+AI工作流重构:六类标记法与IPO基元拆解实战 1. 为什么“一人AI”不是口号,而是一套可拆解的工序很多人第一次听到“工作流重构”这四个字,脑子里浮现的是画流程图、买SaaS工具、拉群对齐。我做了十多年项目,见过太多团队把“重构”搞成了“重画”——流程图贴满一面墙,执行的… · 2026/9/25 18:42:36
CATIA二次开发齿轮自动建模插件:参数化生成与源码避坑指南 简介:面向机械设计工程师、CATIA二次开发人员以及需要快速创建圆柱直斜齿轮、圆锥齿轮或齿轮轴的三维建模用户,资源包内含基于CATIA COM接口开发的插件与完整源码。插件兼容CATIA V5 R14及以上版本,通过设定模数、压力角、齿数等参数即可自动… · 2026/9/25 18:42:12
4款免费磁力下载工具:不限速边下边播+磁力搜索,BT下载迅雷平替,安装包下载 前两天想下一部老电影,打开迅雷发现资源被限制了,速度卡在几十KB,心态直接崩了。翻了半天找到几款免费的磁力下载工具,亲测速度能跑到20MB/s,关键是全都不限速。挑了4款我觉得最值得用的,挨个说说。柚子下载… · 2026/9/25 18:42:12
DeskcommCRM全拆解:从沟通闭环到客户数据资产 去年我帮一家做企业服务的公司搭销售信息底座的时候,发现他们最头疼的不是没有CRM,而是客户聊过的需求散落在微信、邮件、电话、Excel里,谁跟进过、跟到哪一步了、下次该谁接手,完全靠记忆。后来我们做的这套DeskcommCRMÿ… · 2026/9/25 18:42:12
糖尿病预测系统实战:从数据清洗到模型部署的机器学习全流程 简介:这是一份面向计算机相关专业学生与教师的机器学习实战项目源码,以糖尿病预测为应用场景,适合作为课程设计、期末大作业或毕设参考,也可用于项目立项演示与二次开发练习。资源包共46个文件,约55KB,以xm… · 2026/9/25 18:42:06
security-audit-skill漏洞检测 1. 引入
cloudflare开源了一套security-audit-skill,详见参考1。看了一下,发现能检测的漏洞类型还挺多,思路也比较全面,所以对他做一个全面的分析,并记录在此。
2. 用法
根据参考3,可知 Claude Code, Curso… · 2026/9/25 18:42:00
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37