亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

使用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文件。为此,你需要做的是:

  1. 获取你想要画出的任何形状的路径;
  2. 将路径转换成多边形;
  3. 将路径作为数组存储在 topoJSON 中。
步骤 2.0:以 SVG 格式绘制楼层平面图

除非你的楼层图极其简单,否则你需要用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 — 官方网站

https://geojson.org/

TopoJSON — 官方 Github 频道

https://github.com/topojson topology和地理信息相关的GitHub仓库

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消