<audio id="audio" title="03 | 声明式图形系统：如何用SVG图形元素绘制可视化图表？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/42/a5/4256090856681648991eb448e236d6a5.mp3"></audio>

你好，我是月影。今天，我们来讲SVG。

SVG的全称是Scalable Vector Graphics，可缩放矢量图，它是浏览器支持的一种基于XML语法的图像格式。

对于前端工程师来说，使用SVG的门槛很低。因为描述SVG的XML语言本身和HTML非常接近，都是由标签+属性构成的，而且浏览器的CSS、JavaScript都能够正常作用于SVG元素。这让我们在操作SVG时，没什么特别大的难度。甚至，我们可以认为，**SVG就是HTML的增强版**。

对于可视化来说，SVG是非常重要的图形系统。它既可以用JavaScript操作绘制各种几何图形，还可以作为浏览器支持的一种图片格式，来 独立使用img标签加载或者通过Canvas绘制。即使我们选择使用HTML和CSS、Canvas2D或者WebGL的方式来实现可视化，但我们依然可以且很有可能会使用到SVG图像。所以，关于SVG我们得好好学。

那这一节课，我们就来聊聊SVG是怎么绘制可视化图表的，以及它的局限性是什么。希望通过今天的讲解，你能掌握SVG的基本用法和使用场景。

## 利用SVG绘制几何图形

在第1节课我们讲过，SVG属于**声明式绘图系统**，它的绘制方式和Canvas不同，它不需要用JavaScript操作绘图指令，只需要和HTML一样，声明一些标签就可以实现绘图了。

那SVG究竟是如何绘图的呢？我们先来看一个SVG声明的例子。

```
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot;&gt;
  &lt;circle cx=&quot;100&quot; cy=&quot;50&quot; r=&quot;40&quot; stroke=&quot;black&quot;
  stroke-width=&quot;2&quot; fill=&quot;orange&quot; /&gt;
&lt;/svg&gt;

```

在上面的代码中，svg元素是SVG的根元素，属性xmlns是xml的名字空间。那第一行代码就表示，svg元素的xmlns属性值是"[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)"，浏览器根据这个属性值就能够识别出这是一段SVG的内容了。

svg元素下的circle元素表示这是一个绘制在SVG图像中的圆形，属性cx和cy是坐标，表示圆心的位置在图像的x=100、y=50处。属性r表示半径，r=40表示圆的半径为40。

以上，就是这段代码中的主要属性。如果仔细观察你会发现，我们并没有给100、50、40指定单位。这是为什么呢？

因为SVG坐标系和Canvas坐标系完全一样，都是以图像左上角为原点，x轴向右，y轴向下的左手坐标系。而且在默认情况下，SVG坐标与浏览器像素对应，所以100、50、40的单位就是px，也就是像素，不需要特别设置。

说到这，你还记得吗？在Canvas中，为了让绘制出来的图形适配不同的显示设备，我们要设置Canvas画布坐标。同理，我们也可以通过给svg元素设置viewBox属性，来改变SVG的坐标系。如果设置了viewBox属性，那SVG内部的绘制就都是相对于SVG坐标系的了。

好，现在我们已经知道上面这段代码的含义了。那接下来，我们把它写入HTML文档中，就可以在浏览器中绘制出一个带黑框的橙色圆形了。

<img src="https://static001.geekbang.org/resource/image/c0/7f/c0617044406c562834c9e3db9d6d877f.jpg" alt="" title="黑色外框的圆形">

现在，我们已经知道了SVG的基本用法了。总的来说，它和HTML的用法基本一样，你可以参考HTML的用法。那接下来，我还是以上一节课实现的层次关系图为例，来看看使用SVG该怎么实现。

## 利用SVG绘制层次关系图

我们先来回忆一下，上一节课我们要实现的层次关系图，是在一组给出的层次结构数据中，体现出同属于一个省的城市。数据源和前一节课相同，所以数据的获取部分并没有什么差别。这里我就不列出来了，我们直接来讲绘制的过程。

首先，我们要将获取Canvas对象改成获取SVG对象，方法是一样的，还是通过选择器来实现。

```
const svgroot = document.querySelector('svg');

```

然后，我们同样实现draw方法从root开始遍历数据对象。 不过，在draw方法里，我们不是像上一讲那样，通过Canvas的2D上下文调用绘图指令来绘图，而是通过创建SVG元素，将元素添加到DOM文档里，让图形显示出来。具体代码如下：

```
function draw(parent, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
    const {x, y, r} = node;
    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    circle.setAttribute('cx', x);
    circle.setAttribute('cy', y);
    circle.setAttribute('r', r);
    circle.setAttribute('fill', fillStyle);
    parent.appendChild(circle);
    ...
}

draw(svgroot, root);



```

从上面的代码中你可以看到，我们是使用**document.createElementNS方法**来创建SVG元素的。这里你要注意，与使用document.createElement方法创建普通的HTML元素不同，SVG元素要使用document.createElementNS方法来创建。

其中，第一个参数是名字空间，对应SVG名字空间http://www.w3.org/2000/svg。第二个参数是要创建的元素标签名，因为要绘制圆型，所以我们还是创建circle元素。然后我们将x、y、r分别赋给circle元素的cx、cy、r属性，将fillStyle赋给circle元素的fill属性。最后，我们将circle元素添加到它的parent元素上去。

接着，我们遍历下一级数据。这次，我们创建一个SVG的g元素，递归地调用draw方法。具体代码如下：

```
if(children) {
    const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
    for(let i = 0; i &lt; children.length; i++) {
      draw(group, children[i], {fillStyle, textColor});
    }
    parent.appendChild(group);
  }

```

SVG的g元素表示一个分组，我们可以用它来对SVG元素建立起层级结构。而且，如果我们给g元素设置属性，那么它的子元素会继承这些属性。

最后，如果下一级没有数据了，那我们还是需要给它添加文字。在SVG中添加文字，只需要创建text元素，然后给这个元素设置属性就可以了。操作非常简单，你看我给出的代码就可以理解了。

```
else {
    const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    text.setAttribute('fill', textColor);
    text.setAttribute('font-family', 'Arial');
    text.setAttribute('font-size', '1.5rem');
    text.setAttribute('text-anchor', 'middle');
    text.setAttribute('x', x);
    text.setAttribute('y', y);
    const name = node.data.name;
    text.textContent = name;
    parent.appendChild(text);
  }

```

这样，我们就实现了SVG版的层次关系图。你看，它是不是看起来和前一节利用Canvas绘制的层次关系图没什么差别？纸上得来终觉浅，你可以自己动手实现一下，这样理解得会更深刻。

<img src="https://static001.geekbang.org/resource/image/07/bf/072dbcd9607dd7feafa47e34f97784bf.jpeg" alt="" title="层次关系图">

## SVG和Canvas的不同点

那么问题就来了，既然SVG和Canvas最终的实现效果没什么差别，那在实际使用的时候，我们该如何选择呢？这就需要我们了解SVG和Canvas在使用上的不同点。知道了这些不同点，我们就能在合适的场景下选择合适的图形系统了。

SVG和Canvas在使用上的不同主要可以分为两点，分别是**写法上的不同**和**用户交互实现上**的不同。下面，我们一一来看。

### 1. 写法上的不同

第1讲我们说过，SVG是以创建图形元素绘图的“声明式”绘图系统，Canvas是执行绘图指令绘图的“指令式”绘图系统。那它们在写法上具体有哪些不同呢，我们以层次关系图的绘制过程为例来对比一下。

在绘制层次关系图的过程中，SVG首先通过创建标签来表示图形元素，circle表示圆，g表示分组，text表示文字。接着，SVG通过元素的setAttribute给图形元素赋属性值，这个和操作HTML元素是一样的。

而Canvas先是通过上下文执行绘图指令来绘制图形，画圆是调用context.arc指令，然后再调用context.fill绘制，画文字是调用context.fillText指令。另外，Canvas还通过上下文设置状态属性，context.fillStyle设置填充颜色，conext.font设置元素的字体。我们设置的这些状态，在绘图指令执行时才会生效。

从写法上来看，因为SVG的声明式类似于HTML书写方式，本身对前端工程师会更加友好。但是，SVG图形需要由浏览器负责渲染和管理，将元素节点维护在DOM树中。这样做的缺点是，在一些动态的场景中，也就是需要频繁地增加、删除图形元素的场景中，SVG与一般的HTML元素一样会带来DOM操作的开销，所以SVG的渲染性能相对比较低。

那除了写法不同以外，SVG和Canvas还有其他区别吗？当然是有的，不过我要先卖一个关子，我们讲完一个例子再来说。

### 2. 用户交互实现上的不同

我们尝试给这个SVG版本的层次关系图添加一个功能，也就是当鼠标移动到某个区域时，这个区域会高亮，并且显示出对应的省-市信息。

因为SVG的一个图形对应一个元素，所以我们可以像处理DOM元素一样，很容易地给SVG图形元素添加对应的鼠标事件。具体怎么做呢？我们一起来看。

首先，我们要给SVG的根元素添加mousemove事件，添加代码的操作很简单，你可以直接看代码。

```
 let activeTarget = null;
  svgroot.addEventListener('mousemove', (evt) =&gt; {
    let target = evt.target;
    if(target.nodeName === 'text') target = target.parentNode;
    if(activeTarget !== target) {
      if(activeTarget) activeTarget.setAttribute('fill', 'rgba(0, 0, 0, 0.2)');
    }
    target.setAttribute('fill', 'rgba(0, 128, 0, 0.1)');
    activeTarget = target;
  });


```

就像是我们熟悉的HTML用法一样，我们通过事件冒泡可以处理每个圆上的鼠标事件。然后，我们把当前鼠标所在的圆的颜色填充成’rgba(0, 128, 0, 0.1)’，这个颜色是带透明的浅绿色。最终的效果就是当我们的鼠标移动到圆圈范围内的时候，当前鼠标所在的圆圈变为浅绿色。你也可以尝试设置其他的值，看看不同的实现效果。

接着，我们要实现显示对应的省-市信息。在这里，我们需要修改一下draw方法。具体的修改过程，可以分为两步。

第一步，是把省、市信息通过扩展属性data-name设置到svg的circle元素上，这样我们就可以在移动鼠标的时候，通过读取鼠标所在元素的属性，拿到我们想要展示的省、市信息了。具体代码如下：

```
  function draw(parent, node, {fillStyle = 'rgba(0, 0, 0, 0.2)', textColor = 'white'} = {}) {
    ...
    const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
    ...
    circle.setAttribute('data-name', node.data.name);
    ...
    
    if(children) {
     const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
      ...
      group.setAttribute('data-name', node.data.name);
      ...
    } else {
      ...
    }
  }


```

第二步，我们要实现一个getTitle方法，从当前鼠标事件的target往上找parent元素，拿到“省-市”信息，把它赋给titleEl元素。这个titleEl元素是我们添加到网页上的一个h1元素，用来显示省、市信息。

```
 const titleEl = document.getElementById('title');

  function getTitle(target) {
    const name = target.getAttribute('data-name');
    if(target.parentNode &amp;&amp; target.parentNode.nodeName === 'g') {
      const parentName = target.parentNode.getAttribute('data-name');
      return `${parentName}-${name}`;
    }
    return name;
  }


```

最后，我们就可以在mousemove事件中更新titleEl的文本内容了。

```

  svgroot.addEventListener('mousemove', (evt) =&gt; {
    ...
    titleEl.textContent = getTitle(target);
    ...
  });

```

这样，我们就实现了给层次关系图增加鼠标控制的功能，最终的效果如下图所示，完整的代码我放在[GitHub仓库](https://github.com/akira-cn/graphics/tree/master/svg)了，你可以自己去查看。

<img src="https://static001.geekbang.org/resource/image/9c/2f/9c807df0cb2b4afyye6be3345a9c1a2f.gif" alt="">

其实，我们上面讲的鼠标控制功能就是一个简单的用户交互功能。总结来说，利用SVG的一个图形对应一个svg元素的机制，我们就可以像操作普通的HTML元素那样，给svg元素添加事件实现用户交互了。所以，SVG有一个非常大的优点，那就是**可以让图形的用户交互非常简单**。

和SVG相比，利用Canvas对图形元素进行用户交互就没有那么容易了。不过，对于圆形的层次关系图来说，在Canvas图形上定位鼠标处于哪个圆中并不难，我们只需要计算一下鼠标到每个圆的圆心距离，如果这个距离小于圆的半径，我们就可以确定鼠标在某个圆内部了。这实际上就是上一节课我们留下的思考题，相信现在你应该可以做出来了。

但是试想一下，如果我们要绘制的图形不是圆、矩形这样的规则图形，而是一个复杂得多的多边形，我们又该怎样确定鼠标在哪个图形元素的内部呢？这对于Canvas来说，就是一个比较复杂的问题了。不过这也不是不能解决的，在后续的课程中，我们就会讨论如何用数学计算的办法来解决这个问题。

## 绘制大量几何图形时SVG的性能问题

虽然使用SVG绘图能够很方便地实现用户交互，但是有得必有失，SVG这个设计给用户交互带来便利性的同时，也带来了局限性。为什么这么说呢？因为它和DOM元素一样，以节点的形式呈现在HTML文本内容中，依靠浏览器的DOM树渲染。如果我们要绘制的图形非常复杂，这些元素节点的数量就会非常多。而节点数量多，就会大大增加DOM树渲染和重绘所需要的时间。

就比如说，在绘制如上的层次关系图时，我们只需要绘制数十个节点。但是如果是更复杂的应用，比如我们要绘制数百上千甚至上万个节点，这个时候，DOM树渲染就会成为性能瓶颈。事实上，在一般情况下，当SVG节点超过一千个的时候，你就能很明显感觉到性能问题了。

幸运的是，对于SVG的性能问题，我们也是有解决方案的。比如说，我们可以使用虚拟DOM方案来尽可能地减少重绘，这样就可以优化SVG的渲染。但是这些方案只能解决一部分问题，当节点数太多时，这些方案也无能为力。这个时候，我们还是得依靠Canvas和WebGL来绘图，才能彻底解决问题。

那在上万个节点的可视化应用场景中，SVG就真的一无是处了吗？当然不是。SVG除了嵌入HTML文档的用法，还可以直接作为一种图像格式使用。所以，即使是在用Canvas和WebGL渲染的应用场景中，我们也依然可能会用到SVG，将它作为一些局部的图形使用，这也会给我们的应用实现带来方便。在后续的课程中，我们会遇到这样的案例。

## 要点总结

这一节课我们学习了SVG的基本用法、优点和局限性。

我们知道，SVG作为一种浏览器支持的图像格式，既可以作为HTML内嵌元素使用，也可以作为图像通过img元素加载，或者绘制到Canvas内。

而用SVG绘制可视化图形与用Canvas绘制有明显区别，SVG通过创建标签来表示图形元素，然后将图形元素添加到DOM树中，交给DOM完成渲染。

使用DOM树渲染可以让图形元素的用户交互实现起来非常简单，因为我们可以直接对图形元素注册事件。但是这也带来问题，如果图形复杂，那么SVG的图形元素会非常多，这会导致DOM树渲染成为性能瓶颈。

## 小试牛刀

<li>
DOM操作SVG元素和操作普通的HTML元素几乎没有差别，所以CSS也同样可以作用于SVG元素。那你可以尝试使用CSS，来设置这节课我们实现的层级关系图里，circle的背景色和文字属性。接着你也可以进一步想一想，这样做有什么好处？
</li>
<li>
因为SVG可以作为一种图像格式使用，所以我们可以将生成的SVG作为图像，然后绘制到Canvas上。那如果我们先用SVG生成层级关系图，再用Canvas来完成绘制的话，和我们单独使用它们来绘图有什么不同？为什么？
</li>

欢迎在留言区和我讨论，分享你的答案和思考，也欢迎你把这节课分享给你的朋友，我们下节课见！

## 源码

[用SVG绘制层次关系图和给层次关系图增加鼠标控制的完整代码.](https://github.com/akira-cn/graphics/tree/master/svg)
