使用D3和TopoJSON制作簡單的交互式樓層平面圖
照片由Sven Mieke拍摄,来自Unsplash
D3.js 是一个轻量且强大的数据可视化 JavaScript 库。虽然它常用于制作各种图表甚至地图,但它也可以用来制作互动式的楼层平面图,这种平面图可以在一些地方看到,比如购物中心、火车站和机场。
在这篇文章里,我会教你如何使用topoJSON、D3.js和纯JavaScript(以及几个有用的在线工具的帮助)创建一个基本的互动楼层平面图。阅读结束后,你应该能够从SVG文件创建自己的拓扑JSON,在你的脚本中加载它,并实现带有悬停效果的楼层平面图。
欢迎访问我的GitHub查看与本文相关的项目:https://github.com/kamiviolet/d3-topojson-floormap。
难度等级:初学者 ~ 中级
D3.js是什么?D3,简称数据驱动文档(Data-Driven Documents),是一个免费开源的JS库,用于在现代网络浏览器中实现动态和交互式数据可视化。最初于2011年发布,它提供了各种内置功能,从缩放、平移和过渡到过渡动画,使得开发者无需深入算法细节,就能轻松创建各种图表。
由于 jQuery 当时在网页开发中非常普遍,D3.js 采用了与 jQuery 非常相似的语法,使开发人员能够轻松地使用 CSS 选择器来创建、操作和设置 DOM 元素的样式。
什么是GeoJSON?GeoJSON 是存储地理数据的开放标准格式之一。与地理信息系统中的复杂且存储在二进制中的 shapefiles 不同,GeoJSON 使用广泛应用的 JSON 格式来表示较小的地理要素以及它们的非空间属性。
格式大致如下:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [51.50382215630841, -0.11433874098846404]
},
"properties": {
"name": "标识",
"description": "这是一个靠近伦敦滑铁卢的标识。"
}
}
]
}
什么是TopoJSON?
TopoJSON?应调整为TopoJSON,并在句末添加句点,最终结果为:
什么是TopoJSON?
TopoJSON 是 geoJSON 的一种扩展,用于编码地理拓扑。在 TopoJSON 中,几何是通过共享线段(称为 弧线)连接起来的,而不是独立表示每个几何图形。
很重要的一点是不要把这些"弧"和SVG里的弧(通过"A"或"a"命令定义的)搞混了。实际上,geoJSON和topoJSON都不支持SVG弧或贝塞尔曲线,因为它们并不处理这些图形元素。这里的弧是一系列的[x,y]坐标集,这取决于几何类型(下一节会详细解释)。看到的任何曲线都是由多个[x,y]坐标集绘制的。
与上面提到的 geoJSON 格式相比,topoJSON 就是这样的:
{
"type": "拓扑",
"objects": {
"object": {
"type": "顶点",
"arcs": [[0]],
"properties": {
"name": "标记物",
"description": "我是一个没有实际坐标的标记"
}
}
},
"arcs": [
[[100, 100]]
]
}
现在我们对工具已经有了基本的了解,让我们开始项目。
第一步 — 安装Both D3.js 和 topoJSON 可以通过如 npm、yarn 和 pnpm 这样的包管理器安装,或者通过下载最新版本并在脚本或 HTML 文件中引入,或者从 CDN (内容分发网络, CDN) 获取。
D3.js (数据可视化库)如果你已经安装了 Node 在你的环境里,那么最简单的方式是通过 npm 安装。
npm install d3
在命令行中运行此命令来安装d3库
然后,你可以使用 ES6 的 import 语句在你的脚本中导入库。
import * as d3 from "d3";
如果你没有使用任何包管理器,官方建议直接使用 CDN 提供的 ES 模块包。他们还提供一个 UMD 包,允许你将其下载为普通脚本以供离线使用。
/* ESM 加 CDN */
<script type="module">
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
...
</script>
/* UMD 加 CDN (UMD 模块化方式通过 CDN 加载) */
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://cdn.jsdelivr.net/npm/d3@7" type="module"></script>
<script>
...
</script>
/* UMD 加本地(方法1 - HTML)*/
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="./d3.v7.js" type="module"></script>
<script>
...
</script>
/* UMD 加本地(方法2 - JS)*/
<script type="module">
import "./d3.v7.js";
...
</script>
topoJSON 客户端
和 D3 一样,你可以用 npm 运行以下命令来安装 topoJSON-client:
运行此命令来安装topojson-client模块
npm install topojson-client
接下来,如下所示使用 ES6 模块导入将其导入:
// ES6 import module code here
import * as topojson from "topojson-client"; // 导入拓扑JSON客户端库
或者你可以这样加载它
/* 内容分发网络 (CDN) */
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://unpkg.com/topojson-client@3" type="module"></script>
<script>
...
</script>
/* 本地加载 */
<script type="module">
import "./topojson-client.js"; // 加载本地的 topojson-client.js 文件
...
</script>
安装 D3 和 topoJSON-client 应该没什么问题。如果遇到任何困难,,我建议你查看官方文档,或者在下面写下你遇到的问题。
步骤 2 — 数据录入
这一步里,你需要准备好topoJSON文件。为此,你需要做的是:
- 获取你想要画出的任何形状的路径;
- 将路径转换成多边形;
- 将路径作为数组存储在 topoJSON 中。
除非你的楼层图极其简单,否则你需要用SVG来绘制弧线。对于这种需求,我推荐使用Inkscape,因为它是一款功能强大的免费开源SVG编辑软件,并且意外地易于上手(相比它所能做到的事情而言)。
如果你没听说过这个软件,或者对如何使用它感到不确定,你可能需要熟悉一下它的界面,让自己感觉更舒服。网上有很多教程可以参考。关于Inkscape的探索内容很多,所以我现在就先不谈这部分了。
用Inkscape做的一房层平面图
无论你选择哪种软件,甚至你是否更喜欢完全凭直觉画出 [x,y],这些都不重要。我们的目标是简单,即——
接下来我们需要得到填充topoJSON所需的路径集。
完成SVG之后,你可以在Inkscape内置的XML编辑器中找到路径的信息。请确认所有路径都使用了绝对坐标值,即所有的命令都使用大写字母表示(比如M, H, V, Z, L)。 如果不是这样,建议尝试使用下面可选步骤2.0.2中提到的在线工具:
XML版本中:你要找的“d”的值
步骤 2.0.1(如有必要)
绘制形状并将其导出为SVG格式后,别忘了使用SVGOMG或类似的在线SVG校验工具等来优化文件。
通过使用 SVGOMG,文件大小从 1.2kb 减至 350 字节。
第二步 2.0.2(选做)
就我个人而言,我更倾向于避免使用浮点数,除非曲线形状实在无法避免,否则还是保持数值为整数比较好。如果你也有同样的想法,你也可以试试这个工具SvgPathEditor。
SvgPathEditor 允许你粘贴 SVG 路径,然后你可以分别缩放、圆角化和编辑每一个路径,确保圆角化后它们的关系仍然保持一致。此外,它还允许你在绝对命令(M、Z、L、H、V)和相对命令(m、z、l、h、v)之间进行转换。你的所有操作都将实时显示在屏幕的右侧。
SvgPathEditor 是一个不错的在线工具,可以帮助你精细调整 SVG 路径,或者解决线条不对齐的问题。
步骤 2.1:把路径改成正确的格式上一步里,你找到了所有类似于这个字符串的路径。
M 59, 94 H 1140 V 1097 H 59 Z
GeoJSON 和 TopoJSON 都不直接支持路径(paths),而是支持以下七种几何类型:
- 点
- 线串(LineString)
- 多边形
- 多点
- 多线串
- 多边形集合
- 几何集合
为了使用你收集的路径,你需要将它们转换成点(多点)、多段线(多段线)或多边形(多边形),这取决于路径的形状:点表示为[[x,y]],多段线表示为[[x0,y0], [x1,y1]],而多边形表示为[[x0,y0], [x1,y1]… [x0,y0]]。
对于你正在处理的示例楼层平面图,你希望将所有路径转换为多边形。互联网上有免费的在线转换器,例如路径转多边形转换器,这会很有用。我强烈推荐另一个转换器(https://pjrclarke.github.io/SVG_path_to_polygon_JSON/),它甚至允许你直接从Inkscape XML编辑器中进行格式转换。否则,你也可以自己写一个路径转多边形的转换函数。
按照上述示例路径使用,转换后的结果应该像这样:
[[59, 94], [1140, 94], [1140, 1097], [59, 1097], [59, 94]] # 坐标点列表
需要注意的是,如果 SVG 路径没有显式闭合,你需要手动将其闭合,即确保路径的第一个和最后一个点坐标相同。
2.2 步骤:创建一个拓扑JSON文件现在你有一组数组,包含了你需要的所有楼层坐标,下一步就是生成topoJSON文件。
在 topoJSON 格式中,有三个必需的字段:“type”,“objects” 和 “arcs”。“type” 的值始终是 ‘Topology’,“objects” 是一个包含键及其关联对象的集合,最后是 “arcs”,用于存储所有在 “objects” 中使用的路径,是一个嵌套数组。除了这些字段外,还可以根据需求添加 “bbox” 和 “transform”。
建议您在建立 topoJSON 文件时确保查阅 TopoJSON 格式规范文档。
回到你用 Inkscape 创建的楼层地图,现在你应该首先将所有的多边形放入“弧”内。接着,你将创建对象并使用这些数组。
没有绝对的方式来组织这些数据,我将把楼层地图划分为3个对象,即“楼层”、“区域”和“入口”这三个。完成的topoJSON如下所示:
{
"type": "Topology",
"objects": {
"apartment": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [[0]],
"properties": {
"id": 0,
"type": "楼层平面"
}
}
]
},
"areas": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [[1]],
"properties": {
"id": 2,
"type": "起居室"
}
},
{
"type": "Polygon",
"arcs": [[2]],
"properties": {
"id": 2,
"type": "卫生间"
}
},
{
"type": "Polygon",
"arcs": [[3]],
"properties": {
"id": 3,
"type": "杂物间"
}
},
{
"type": "Polygon",
"arcs": [[4]],
"properties": {
"id": 4,
"type": "卧室"
}
},
{
"type": "Polygon",
"arcs": [[5]],
"properties": {
"id": 5,
"type": "厨房"
}
},
{
"type": "Polygon",
"arcs": [[6]],
"properties": {
"id": 6,
"type": "过道"
}
}
]
},
"entrances": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Polygon",
"arcs": [[7]],
"properties": {
"id": 2,
"type": "主要入口"
}
},
{
"type": "Polygon",
"arcs": [[8]],
"properties": {
"id": 3,
"type": "卧室入口"
}
},
{
"type": "Polygon",
"arcs": [[9]],
"properties": {
"id": 4,
"type": "杂物间入口"
}
},
{
"type": "Polygon",
"arcs": [[10]],
"properties": {
"id": 5,
"type": "卫生间入口"
}
}
]
}
},
"arcs": [
[[1, 1], [1082, 1], [1082, 1004], [1, 1004], [1, 1]],
[[31, 577], [446, 577], [446, 683], [461, 683], [461, 982], [31, 982], [31, 577]],
[[793, 19], [1054, 19], [1054, 479], [793, 479], [793, 19]],
[[619, 262], [764, 262], [764, 481], [619, 481], [619, 262]],
[[31, 19], [446, 19], [446, 563], [31, 563], [31, 19]],
[[606, 496], [1055, 496], [1055, 982], [606, 982], [606, 496]],
[[461, 19], [461, 982], [606, 982], [606, 243], [772, 243], [772, 19], [461, 19]],
[[486, 1], [486, 19], [587, 19], [587, 1], [486, 1]],
[[446, 119], [446, 197], [461, 197], [461, 119], [446, 119]],
[[606, 324], [606, 417], [619, 417], [619, 324], [606, 324]],
[[772, 101], [772, 184], [793, 184], [793, 101], [772, 101]]
]
}
第三步 — 编写脚本,
数据文件准备好后,终于可以开始了,调用D3.js和topoJSON里的函数来看看效果吧。
步骤 3. 准备 HTML 文件在安装期间,你可能已经准备好 index.html 文件了(如果没有,请现在创建一个),它看起来类似于以下代码段。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>互动楼层图</title>
</head>
<body>
<!-- 是否在这里直接硬编码一个DIV元素由你自己决定-->
<div id="svg_container"></div>
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="./script.js" type="module"></script>
</body>
</html>
步骤 3.1 声明核心常量值
在 script.js 中,确保引入 D3 和 topoJSON-client。
```引入 "./lib/d3.v7.js"; // 导入d3.v7.js库
```引入 "./lib/topojson-client.js"; // 导入topojson-client.js库
在导入语句下方,声明一个常量“CANVAS”,这个常量是用来保存你要创建的SVG的宽度和高度这两个属性值的对象。
const CANVAS = {
w: 1000,
h: 1000
}; // 设置画布宽度为1000,高度为1000
接着,你可以定义一个常量‘data’来存储从 json 文件中获取的值。你可以用 D3 的 fetch 函数,或者原生的 fetch API,甚至可以用 XMLHttpRequest 来获取数据。
作为良好的实践,记得在任何获取失败时也加上一个捕获语句。
const data = await d3
.json(`./one_bedroom.json`)
.catch(e => console.error(e.name));
感谢 topoJSON,我们不需要为地图绘制 [x,y] 这样的真实坐标。不过,D3.js 并不能直接处理 topoJSON 文件。幸运的是,你可以通过 topoJSON-client 提供的内置方法 topojson.feature(dataset, key) 轻松将其转换回 geoJSON。
由于 one-bedroom.json 文件中的 'objects' 数组里有 3 个对象,你需要把这 3 个对象都遍历一遍。
// 创建一个空对象用于存储数据
const geoData = {};
// 获取 ['apartment', 'areas', 'entrances'] 这个数组
const arrOfKeys = Object.keys(topoData.objects);
// 遍历 arrOfKeys,并为 geoData 创建一个键来存储对应的 geojson 信息
arrOfKeys.forEach(key => {
geoData[key] = topojson.feature(topoData, key);
})
步骤 3.2(重要!) 创建 D3 路径对象
在你获取数据并将其转换为 D3.js 可读的格式之后,有一个重要的步骤是让 D3.js 生成 SVG 路径——即 创建一个 d3.geoPath 的实例,这是一个生成器,可以将几何对象转换成 SVG 路径数据。
地理路径生成器 geoPath 可以接受给定的 GeoJSON 几何或特征对象,生成 SVG 路径数据字符串,并可以直接将路径渲染到 Canvas。路径可以与投影或变换一起使用,也可以直接将平面几何渲染到 Canvas 或 SVG。(路径 | Observable 中的 D3)
使用 d3.geoPath,可以实现更多功能,比如投影和变换,例如可以实现投影和变换以达到类似平移及缩放的效果。
// 用于实现 d3.projection 方法的身份
const d3Identity = d3.geoIdentity();
// 此方法将投影的缩放设置为适应给定大小的中心对象。
const d3Projection = d3Identity
.fitSize([canvas.w, canvas.h], geoData["apartment"]);
// 将投影传递给生成器进行处理
const d3Path = d3.geoPath(d3Projection);
3.3 画出 SVG 和路径
设置好 d3.geoPath 后,在获取并存储数据后,接下来就是创建 SVG 并设置其属性了。
// 生成 SVG 元素
const svgContainer = d3
.select("#svg_container")
.append("svg")
.attr("viewBox", `${CANVAS.w} ${CANVAS.h}`)
.classed("floormap", true)
// 基于 "objects" 的键创建 g 元素
const groups = svgContainer
.selectAll("g")
.data(arrOfKeys)
.enter()
.append("g")
.attr("class", (d) => d)
// 在每个 g 元素中创建路径
const assets = groups
.selectAll("path")
.data((d) => geoData[d]?.features)
.enter()
.append("path")
.attr("d", d3Path)
一旦你用 D3.js 绘制了 SVG 并在浏览器里查看,……却发现变成了这样?
屏幕黑了?!
默认情况下,SVG 元素的填充颜色为黑色(即 { fill: black })。一种解决方法是继续使用 D3 添加基本样式,以便查看刚创建的路径。有时这种方法在值是动态时是必要的。否则,最好在样式表中统一设置 DOM 元素(包括 SVG)的样式。
步骤 3.4:更新样式表文件在创建 CSS 文件之前,请记得在 index.html 文件中添加 <link> 标签:
<head>
...
<link rel="stylesheet" href="./styles.css" type="text/css" />
<!-- 此处链接了一个样式表文件,用于定义网页的样式。 -->
</head>
为了简单起见,这里是一个简单的 styles.css 文件:
/* styles.css的内容 */
/* 重置浏览器默认设置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: grid;
place-items: 中心对齐;
height: 100vh;
}
#svg_container {
width: 80视口宽度;
height: 100%;
}
.楼层平面图 {
width: 100%;
height: 100%;
}
.公寓 {
fill: gray;
}
.areas, .entrances {
fill: white;
&>*:hover, &>*:active {
fill: orange;
/* 鼠标悬停或点击时的填充颜色 */
}
}
第 4 步 — 在浏览器里仔细检查
现在如果你回到浏览器,你应该能看到与在 Inkscape 中绘制的完全相同的 SVG 图像。当你把它悬停时,不同的部分会被像下面这样突出显示:
将鼠标悬停在一居室的平面图上
结论部分你可能想知道,为什么使用 D3.js 还需要先手动绘制图像。刚开始确实用 topoJSON 和 D3 输入数据会比较耗时;不过,有了 topoJSON,你可以更方便地整理那些非空间属性。
不必多说,D3.js 还提供了许多其他功能,这些功能本文没有提到,比如平移、缩放以及响应式标记和图标等。一旦你有了 topoJSON 文件,就可以轻松地将地图与诸如 json、csv 或 xml 等外部数据进行整合,相比之下,如果将地图作为图片绘制并加载到文档中,将会更加困难。
除此之外,你甚至可以将地图升级到三维。将D3.js与Three.js集成是完全可以的,添加更多功能,并创建一个互动3D楼层平面图。
使用 D3.js 创建楼层地图也有一些缺点,比如,除此之外,扩展性可能成为一个问题,特别是在项目变大时,SVG 的性能可能成为一个瓶颈。
你之前用过 D3.js 或者 topoJSON 吗?你觉得这些工具怎么样?欢迎下面留言分享你的看法~
另一方面,我尽力保证信息的准确性,如果您在这篇文章中发现任何需要更正或补充的地方,请在这里留言告诉我。
谢谢大家的阅读!
相关链接: 外部链接: D3 — 官网 D3 - JavaScript 数据可视化工具库 d3js.org GeoJSON — 官方网站 TopoJSON — 官方 Github 频道https://github.com/topojson topology和地理信息相关的GitHub仓库
共同學習,寫下你的評論
評論加載中...
作者其他優質文章