ARTICLE · 1223479
我把「一张 Excel 表格」变成了公司墙上最贵的那张图——深度拆解 ThoughtWorks 开源技术雷达 Build Your Own Radar
一、先说结论:它凭什么值得你花一小时读完
先说一个场景,你一定经历过。
季度技术复盘会上,讨论「我们用不用这个框架」,白板上画了三个圈、四条线,画完五分钟就被擦掉。下周来了新同事,没人记得当时的结论是怎么来的。
问题的本质不是「缺一张图」,而是缺一张「活着的图」。
ThoughtWorks 每年发布的 Technology Radar(技术雷达)就是解决这个问题的标杆。它把技术分成四个环、四个象限,用一张漂亮又信息密度极高的图告诉全世界:哪些技术在采纳、哪些在试验、哪些要谨慎。
而 ThoughtWorks 直接把这套方法论开源成了一个工具——build-your-own-radar(简称 BYOR)。你只要有一份符合格式的表格,粘一个链接进去,就能生成一张和你公司 Logo 一个风格的技术雷达。
它背后的理念一句话概括:
数据源即配置。 你不需要写前端、不需要买服务,只要维护一张表,雷达随时刷新。
一张图看懂它能做什么:

二、它到底是什么:一句话 + 一个背景
一句话定义: Build Your Own Radar 是一个开源的、用来生成「交互式技术雷达」的库,灵感来自 ThoughtWorks 官方的 Technology Radar。
它最妙的地方在于——它不是一个「画图工具」,而是一个「数据渲染引擎」。你给它结构化数据,它负责把数据变成一张会呼吸的图。

它的技术栈非常克制,几乎全是成熟前端生态的「老熟人」:
| D3.js v7 | ||
| sanitize-html | ||
许可证是 AGPL-3.0,当前主版本为 1.2.1。这两个信息很重要,后面「商用注意」一节会专门讲。
三、数据格式:三分钟就能上手的「万能表格」
BYOR 支持的输入格式有三种,但它们的列结构完全一致。你只要记住这几列,就掌握了全部。
必需列:
name | ||
ring | ||
quadrant | ||
description | ||
isNew |
关键点: 表格里 isNew 或 status至少要有一个,否则校验不通过。
可选的 status 列(用来展示光点移动轨迹),接受以下大小写不敏感的值:
New:第一次出现在雷达上 Moved In:向圆心移动(采纳度上升) Moved Out:向边缘移动(采纳度下降) No Change:位置没变
三种数据源的写法分别是:
① Google Sheet——把表格设为「Anyone with the link → Viewer」,然后把链接粘进去即可。私有表格会触发 Google One Tap 登录,授权后同样能生成。
② CSV——一个可公开访问的 CSV 链接,格式与表格完全一致:
name,ring,quadrant,isNew,descriptionComposer,adopt,tools,TRUE,"Although the idea of dependency management ..."Canary builds,trial,techniques,FALSE,"Many projects have external code dependencies ..."Apache Kylin,assess,platforms,TRUE,"Apache Kylin is an open source analytics solution ..."JSF,Caution,languages & frameworks,FALSE,"We continue to see teams run into trouble using JSF ..."
③ JSON——一个由对象数组构成的 JSON,字段同上:
[{"name": "Composer","ring": "adopt","quadrant": "tools","isNew": "TRUE","description": "Although the idea of dependency management ..."}]
一个必须记住的细节: 官方托管的
radar.thoughtworks.com对ring和quadrant的名称有固定要求(就是上面那套官方命名)。而你自己私有部署的 BYOR 没有这个限制,可以用环境变量自定义名称。
四、功能全景:六个能力,环环相扣
在动手之前,先把它的能力地图看清楚(见开篇的「功能全景图」)。它看起来简单,其实是六块能力拼出来的完整闭环:
- 三源数据接入
:Google Sheet(公开/私有)、CSV、JSON,一条链接搞定; - 四环 × 四象限建模
:把零散的技术条目结构化归档; - 智能防碰撞布点
:随机撒点 + 迭代避让,保证图面整洁; - 交互式雷达
:关键字搜索、象限导航、悬停高亮双向联动; - 移动状态标记
:New / Moved In / Moved Out / No Change 四种轨迹标识; - 一键部署
:Docker 镜像或本地 npm,附 PDF 打印导出。
从数据接入、建模,到布点、交互,再到状态标记与部署,六块能力缺一不可。
五、模块架构:七层分层,各司其职
很多人第一次看这个仓库会懵——src 下文件不少。但一旦你看懂它的分层,整个项目就瞬间清晰了。
它从入口到渲染一共分成七层,每一层只做一件事:

读图要点:
- 入口层
负责把页面挂起来, site.js里其实只有三行核心调用; - 调度层
( factory.js)是整个项目的「交通枢纽」,它决定「数据从哪来、走哪条路」; - 数据接入层
把三种数据源统一抽象(Google Sheet 走 gapi,CSV/JSON 走 d3); - 校验清洗层
是安全底线,负责表头校验、HTML 白名单、环名归一化; - 领域模型层
把原始行数据变成 Radar / Quadrant / Ring / Blip四类对象; - 渲染交互层
才是 D3 真正发力的地方; - 样式资源层
收尾,负责观感。
六、系统架构:纯前端 + 静态托管 + 外部服务
它没有后端数据库,架构干净得不像话。整体是三层:

核心设计哲学: 前端与数据服务彻底解耦。换个数据源,不需要动渲染核心一行代码——这正是「数据源即配置」的工程化落地。
七、核心运行流程:从粘贴 URL 到一张雷达
接下来是最关键的一环。当你在首页粘贴一个链接并点击「Build my radar」后,到底发生了什么?

整个链路可以浓缩成七步:
- 解析查询参数
:从 URL 里取出 sheetId/documentId/sheetName; - 判定数据源类型
:结尾是 .csv或.json走对应分支,域名含google.com则走 Google Sheet; - 拉取原始数据
:Google Sheet 用 Sheets API v4,CSV/JSON 用 d3 请求; - 校验表头
:缺列直接抛错,进入错误提示页; - 清洗与归一化
:白名单过滤 HTML,把历史环名 Hold统一成Caution; - 构建领域模型并布点
:生成 Blip → 计算坐标 → 防碰撞; - 渲染 SVG 雷达
:环形弧、光点、搜索框、象限列表全部就位。
八、时序图:一次完整调用的「时间线」
如果流程图像是「地图」,那时序图就是「行车记录仪」。下面这张图展示了用户点击之后,各模块的调用顺序:

注意两个细节:
- 鉴权是按需加载的
——只有访问私有 Google Sheet 时才真正加载 Google 的鉴权脚本; **自我调用(self-call)**有两处:一次是「校验 + 清洗」,一次是「布点防碰撞」,它们都是纯计算,不依赖网络。
九、重难点源码拆解:真正值钱的四个地方
好,前面都是「看得见的部分」。接下来是这篇文章真正值钱的地方——四个核心难点。看懂它们,你不仅能用好 BYOR,还能把它改造成任何你想要的样子。
难点一:调度中枢 factory.js——一行判断搞定三种数据源
这是整个项目的「大脑」。它的分流逻辑优雅到令人发指:
self.build = function () {// 第一步:校验配置(象限必须是 4 个,环数在 1~4 之间)if (!isValidConfig()) {plotError(new InvalidConfigError(ExceptionMessages.INVALID_CONFIG))return}// 第二步:解析域名与参数,决定走哪条路const domainName = DomainName(window.location.search.substring(1))const paramId = getDocumentOrSheetId()if (paramId && paramId.endsWith('.csv')) {sheet = CSVDocument(paramId) // CSV 分支sheet.init().build()} else if (paramId && paramId.endsWith('.json')) {sheet = JSONFile(paramId) // JSON 分支sheet.init().build()} else if (domainName && domainName.endsWith('google.com') && paramId) {sheet = GoogleSheet(paramId, getSheetName()) // Google Sheet 分支sheet.init().build()} else {setDocumentTitle() // 都没有 → 渲染首页表单}}
为什么这样写值得学? 它把一个复杂的「多态」问题,降维成了字符串后缀判断。三种数据源对外暴露的都是同一个接口 init().build(),调用方完全无感。
难点二:防碰撞布点算法——「随机撒点」也能撒得整整整齐齐
这是本项目最硬核、最容易劝退的部分。技术雷达里几十上百个光点,如果随机乱放,就会挤成一团、互相盖住、根本没法看。
BYOR 的解法是:随机撒点 + 碰撞检测 + 迭代重试。

核心思路用大白话讲:
先在某个环的可用半径区间里,随机生成一个极坐标点,转成直角坐标; 拿这个点跟同环内已有的所有点两两测距; 如果距离太近(发生碰撞),就重新撒一个; 最多重试 200 次( maxIterations = 200)。
碰撞判断的源码非常直白:
function thereIsCollision(coordinates, allCoordinates, blipWidth) {return allCoordinates.some(function (currentCoordinates) {return (Math.abs(currentCoordinates.coordinates[0] - coordinates[0]) <currentCoordinates.width / 2 + blipWidth / 2 + 10 &&Math.abs(currentCoordinates.coordinates[1] - coordinates[1]) <currentCoordinates.width / 2 + blipWidth / 2 + 10)})}
两个魔鬼细节:
它用的是 Chance而不是Math.random(),并且喂了一个确定性的种子——这保证了同样的数据每次生成的图完全一致,不会「刷新一下就变样」;如果 200 次都没找到位置,旧版本会自动缩小光点宽度(减 1 像素、缩放系数最低到 0.7)再递归重试——这是一种「实在挤不下就变小一点」的柔性降级。
难点三:环半径计算——一条「黄金数列」决定美观度
光点放在第几环、环与环之间的间距,全靠一个叫 ringCalculator 的小工具。它用一个巧妙的数列控制半径比例:
const RingCalculator = function (numberOfRings, maxRadius) {var sequence = [0, 6, 5, 3, 2, 1, 1, 1] // 每个环的相对宽度权重self.getRadius = function (ring) {var total = self.sum(numberOfRings) // 总权重var sum = self.sum(ring) // 截止到当前环的累计权重return (maxRadius * sum) / total // 按比例分配半径}return self}
为什么不是平均分? 因为越靠圆心的环,面积越小,能放的点越少。用递减的权重数列,可以让外环更宽敞、内环更紧凑,视觉上更符合「越往中心越核心」的直觉,也更方便布点。
难点四:安全清洗——白名单过滤,一行都不能省
雷达的 description 允许写一点 HTML(加粗、链接等)。但直接把用户输入塞进页面,就是典型的 XSS 漏洞。
BYOR 的做法是白名单——只放行明确的标签,其余一律清洗掉:
const relaxedOptions = {allowedTags: ['b', 'i', 'em', 'strong', 'a', 'h1', 'h2', 'h3','h4', 'h5', 'h6', 'li', 'ul', 'br', 'p', 'u'],allowedAttributes: {a: ['href', 'target', 'rel'],},}// 而 name / ring / quadrant 等字段用 restrictedOptions,allowedTags 为空数组 → 全部剥离
注意这个设计: 不同的列用不同严格程度的规则。描述允许富文本(relaxedOptions),而名称、环名、象限名则完全不允许任何标签(restrictedOptions)。这种「按字段分级授权」的思路,非常值得在你自己的项目里复用。
十、快速上手:三种姿势,任选其一
理论讲完,立刻上手。BYOR 提供三条完全不同的使用路径。
姿势一:本地开发(改代码必用)
git clone git@github.com:thoughtworks/build-your-own-radar.gitcd build-your-own-radarnpm installnpm run dev # 启动后访问 http://localhost:8080,改动实时热更新
想看质量报告?一条命令跑完 lint 和单元测试:
npm run quality姿势二:Docker 一键起(最快看到效果)
官方已经发布了 Docker 镜像,一条命令就能跑起来:
docker pull wwwthoughtworks/build-your-own-radardocker run --rm -p 8080:80 -e CLIENT_ID="[Google Client ID]" wwwthoughtworks/build-your-own-radar:latest# 然后打开 http://localhost:8080
如果你不想把 CSV/JSON 传到公网,可以挂载本地目录,把文件放进 files/,然后访问 http://localhost:8080/files/你的文件.csv:
docker run --rm -p 8080:80 -e SERVER_NAMES="localhost 127.0.0.1" -v /mnt/radar/files/:/opt/build-your-own-radar/files wwwthoughtworks/build-your-own-radar:latest姿势三:不想装 Node?一行 Docker 跑源码
docker run -p 8080:8080 -v $PWD:/app -w /app -it node:24 /bin/sh -c 'npm install && npm run dev'十一、进阶配置:让它长成「你公司专属」的样子
BYOR 用环境变量来外部化所有个性化配置。这是它工程化程度高的体现。
CLIENT_ID | ||
API_KEY | ||
GTM_ID | ||
ADOBE_LAUNCH_SCRIPT_URL | ||
RINGS | ||
QUADRANTS | ||
ASSET_PATH | ||
SERVER_NAMES |
自定义环名和象限名长这样:
export RINGS='["Adopt", "Trial", "Assess", "Caution"]'export QUADRANTS='["Techniques", "Platforms", "Tools", "Languages & Frameworks"]'
两个容易踩的坑:
设置 CLIENT_ID时,一定要在 Google 开发者控制台把 「Authorized JavaScript origins」 配成你的实际域名(带端口),例如http://localhost:8080;象限数量必须正好 4 个,环数量必须在 1~4 之间,否则页面会直接报配置错误。
十二、一个容易被忽略的兼容设计:Hold → Caution
这是最近版本一个非常重要的变更,也是很多老用户升级后会「懵」的地方。
ThoughtWorks 官方技术雷达把最外圈的名字从 Hold 改成了 Caution。而 BYOR 为了向前兼容,做了一个「环名归一化」:
表格里写 Hold(任意大小写),会被自动显示为Caution;这个行为由一个功能开关控制,叫 normalizeRingNameHoldToCaution,默认开启;如果你希望只接受精确的 Caution,可以在配置里把它设为false。
源码里就是一行非常克制的判断:
function normalizeRingNameHoldToCaution(ring) {if ((ring || '').trim().toLowerCase() === 'hold') {return 'Caution'}return ring}
工程启示: 当你的产品需要「对外改名」时,别忘了给老数据留一条兼容通道——这能避免大量存量用户一夜之间「图坏了」。
十三、工程化与质量保障:为什么它值得被信任
一个「能跑的 Demo」和一个「能被全球团队使用」的开源项目,差别就在工程化。BYOR 在这方面做得很扎实:
- 构建
:Webpack 5 打包,CSS 用 mini-css-extract-plugin抽离,文件名带contenthash,天然支持缓存; - 配置注入
:用 DefinePlugin把环境变量「编译进」产物,前端零运行时配置开销; - 单元测试 + 端到端测试
:Jest 跑单测,Cypress 跑 E2E; - 持续集成
:CircleCI 自动跑 lint、单测,并在非 PR 场景下自动构建推送 Docker 镜像; - 代码规范
:ESLint + Prettier 强制统一风格。
端到端测试有两条命令(需要先设 TEST_URL 环境变量):
export TEST_URL='http://localhost:8080'npm run test:e2e-headless # 无头模式,适合 CInpm run e2e # 调试模式,手动选浏览器
注意: 目前 E2E 测试不支持私有 Google Sheet(因为要处理 Google One Tap 弹窗)。跑公开表格的 E2E 时,需要同时设置
CLIENT_ID和API_KEY,给 Cypress 一个已鉴权的会话。
十四、踩坑指南:来自源码的「血泪经验」
这些都是读源码能发现的真实约束,帮你少走弯路:
- 表头必须是
name/ring/quadrant/description,且 isNew或status至少有一个,否则直接报MISSING_HEADERS; - 象限数量必须为 4
,少于 4 个会抛 LESS_THAN_FOUR_QUADRANTS,超过 4 个抛TOO_MANY_QUADRANTS; - 环最多 4 个
,第 5 个环会抛 TOO_MANY_RINGS; - 输入会先 trim 再清洗
,所以列名或值两边的空格不会出问题; status字段是「移动信息」的来源,想展示光点移动轨迹就必须加它; - 自托管没有名称限制
,但官方站点对环名/象限名有固定枚举; - CSV/JSON 的解析依赖 D3
,格式细节可参考 D3 的请求文档。
十五、结语:一张图背后的方法论
写到这里,我想聊聊「它为什么值得被更多人知道」。
Build Your Own Radar 表面上是一个「把表格变成图」的工具,但它真正打动人的,是背后的方法论:
- 把复杂的判断标准化
——四环四象限,让主观的「这个技术好不好」变成了可讨论、可沉淀的结构化结论; - 把数据源和呈现解耦
——你只管维护一张表,剩下的交给引擎; - 把工程细节做扎实
——防碰撞、确定性随机、安全白名单、兼容归一化,每一个细节都在为「好用」服务。
它最狠的地方在于:你不需要成为可视化专家,就能拥有专家级的呈现。
所以,如果你正在带团队、正在做技术选型、正在被「我们到底该不该用这个技术」折磨——别再画白板了。
去建一张属于你们自己的技术雷达吧。 也许下个季度,它就会成为贴在墙上、被反复引用的那张图。
附:快速回忆清单
数据源:Google Sheet / CSV / JSON(列结构统一) 必需列: name、ring、quadrant、description+(isNew或status)四环:Adopt / Trial / Assess / Caution;四象限:Techniques / Platforms / Tools / Languages & Frameworks 核心文件: factory.js(调度)、blips.js(布点)、ringCalculator.js(半径)、inputSanitizer.js(安全)部署: npm run dev或 Docker 镜像wwwthoughtworks/build-your-own-radar许可证:AGPL-3.0















































































