我有一条线,可以连接全世界!

相信我,看完这篇文章,你就会画各种线了。

开始之前,先创建一张画布,文章使用的 d3 是 v5 版本:

  1. const width = 500, height = 500;
  2. const svg = d3.select('body')
  3. .append('svg')
  4. .attr('width', width)
  5. .attr('height', height);

为了方便观察,在开始和结束位置分别画两个圆

  1. const x1 = 10, y1 = 10, x2 = 200, y2 = 200; // 开始,结束点的坐标值
  2. // 开始点
  3. svg
  4. .append('circle')
  5. .attr('cx', x1)
  6. .attr('cy', y1)
  7. .attr('r', 5)
  8. .style('fill', 'none')
  9. .style('stroke', 'purple')
  10. .style('stroke-width', 2);
  11. // 结束点
  12. svg
  13. .append('circle')
  14. .attr('cx', x2)
  15. .attr('cy', y2)
  16. .attr('r', 5)
  17. .style('fill', 'none')
  18. .style('stroke', 'purple')
  19. .style('stroke-width', 2);

1. 直线

最简单的线条:直线。

以下代码展示了用直线将两点连接起来:

  1. const straightLineGroup = svg.append('g');
  2. straightLineGroup.append('path')
  3. .attr('d', 'M' + x1 + ',' + y1 + ' L' + x2 + ',' + y2)
  4. .style('fill', 'none')
  5. .style('stroke', 'green')
  6. .style('stroke-width', 2);

效果如下:
line1.jpg
这段代码最关键的部分就是:

  1. straightLineGroup.append('path')
  2. .attr('d', 'M' + x1 + ',' + y1 + ' L' + x2 + ',' + y2)

d 属性描述了连线路径。

M x,y 在这里x和y是绝对坐标,分别代表水平坐标和垂直坐标。
m dx,dy 在这里dx和dy是相对于当前点的距离,分别是向右和向下的距离。

Moveto指令不同,Lineto指令将绘制一条直线段。这个直线段从当前位置移到指定位置。原生的Lineto命令的句法是”L x, y“或者”l dx, dy“,在这里x和y是绝对坐标,而dx和dy分别是向右和向下的距离。还有字母H和V,分别指定水平和垂直移动。它们的句法与L相同,它的小写版本是相对距离,大写版本是绝对位置。

2. 折线

2.1 简单的折线

简单的折线有四个连接点,起点 ( x1, y1 ),第一个折线点: ( (x1+x2)/2, y1 ), 第二个折线点:( (x1+x2)/2, y2 ),终点:(x2, y2)

  1. const polygonalLineGroup = svg.append('g');
  2. polygonalLineGroup.append('path')
  3. .attr('d', 'M' + x1 + ',' + y1 + ' L' + (x1+x2)/2 + ',' + y1 + ' L' + (x1+x2)/2 + ',' + y2 + ' L' + x2 + ',' + y2)
  4. .style('fill', 'none')
  5. .style('stroke', 'red')
  6. .style('stroke-width', 2);

效果如下:

polygonal1.jpg
换个方向折

  1. polygonalLineGroup.append('path')
  2. .attr('d', 'M' + x1 + ',' + y1 + ' L' + x1 + ',' + (y1+y2)/2 + ' L' + x2 + ',' + (y1+y2)/2 + ' L' + x2 + ',' + y2)
  3. .style('fill', 'none')
  4. .style('stroke', 'red')
  5. .style('stroke-width', 2);

polygonal2.jpg

给线条加个属性,让连接点圆滑一点

  1. polygonalLineGroup.append('path')
  2. ...
  3. .style('stroke-linejoin', 'round')

polygonal3.jpg============> polygonal4.jpg

放大一点可以看到连接的直角变得圆滑了。

2.2 带弧度的折线

但是有时候,我们需要连接的角度是有弧度的,像这样:

polygonal5.jpg

要构造这样的线条需要先了解线条的弧形指令的使用:

  1. A rx ry x-axis-rotation large-arc-flag sweep-flag x y
  2. a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy

这个指令里面最需要理解的 large-arc-flag sweep-flag 这两个参数对弧形的影响:large-arc-flag(角度大小) 和sweep-flag(弧线方向),large-arc-flag决定弧线是大于还是小于180度,0表示小角度弧,1表示大角度弧。sweep-flag表示弧线的方向,0表示从起点到终点沿逆时针画弧,1表示从起点到终点沿顺时针画弧。下面的例子展示了这四种情况。

polygonal7.png

  1. <?xml version="1.0" standalone="no"?>
  2. <svg width="325px" height="325px" version="1.1" xmlns="http://www.w3.org/2000/svg">
  3. <path d="M80 80
  4. A 45 45, 0, 0, 0, 125 125
  5. L 125 80 Z" fill="green"/>
  6. <path d="M230 80
  7. A 45 45, 0, 1, 0, 275 125
  8. L 275 80 Z" fill="red"/>
  9. <path d="M80 230
  10. A 45 45, 0, 0, 1, 125 275
  11. L 125 230 Z" fill="purple"/>
  12. <path d="M230 230
  13. A 45 45, 0, 1, 1, 275 275
  14. L 275 230 Z" fill="blue"/>
  15. </svg>

如果想要好好理解一下路径的Arcto指令,请查看这篇文章https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths

好了,接下来我们就开始画一条这样的折线:

  1. // 水平折线路径构造器
  2. function hArcLineGenerater (x1, y1, x2, y2, radius) {
  3. return "M" + x1 + "," + y1
  4. + "h" + ((x2-x1)/2 - radius)
  5. + "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
  6. + "v" + (y2-y1-2*radius)
  7. + "a" + radius + "," + radius + " 0 0 0 " + radius + "," + radius
  8. + "L" + x2 + "," + y2;
  9. }
  10. // 绘制路径
  11. const radius = 20;
  12. polygonalLineGroup.append("path")
  13. .attr("d", hArcLineGenerater(x1, y1, x2, y2, radius))
  14. .style('fill', 'none')
  15. .style('stroke', 'red')
  16. .style('stroke-width', 2);

这段代码的主要关键是路径生成函数 hArcLineGenerater 生成的折线路径。

效果如下:

polygonal5.jpg

换个方向再折一下:

  1. // 垂直折线路径构造器
  2. function vArcLineGenerater (x1, y1, x2, y2, radius) {
  3. return "M" + x1 + "," + y1
  4. + "v" + ((y2-y1)/2 - radius)
  5. + "a" + radius + "," + radius + " 0 0 0 " + radius + "," + radius
  6. + "h" + (x2-x1-2*radius)
  7. + "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
  8. + "L" + x2 + "," + y2
  9. }
  10. // 绘制路径
  11. const radius = 20;
  12. polygonalLineGroup.append("path")
  13. .attr("d", vArcLineGenerater(x1, y1, x2, y2, radius))
  14. .style('fill', 'none')
  15. .style('stroke', 'red')
  16. .style('stroke-width', 2);

效果如下:

polygonal6.jpg

3. 贝塞尔曲线

贝塞尔曲线有个控制点的概念,具体可以看这里:https://developer.mozilla.org/zh-CN/docs/Web/SVG/Tutorial/Paths

以下二次贝塞尔曲线是基于 d3 绘制,非常简单,不需要自己构造路径算法。

3.1 二次贝塞尔曲线

为了更方便观察,我绘制了控制点, 橘黄色的圆点为控制点

  1. const dx = x1 - 10, dy = y2 - 10;
  2. const cpx1 = x1 - dx;
  3. const cpy1 = y1 + dy;
  4. const cpx2 = x2 + dx;
  5. const cpy2 = y2 - dy;
  6. const bezierLineGroup = svg.append('g');
  7. const path = d3.path();
  8. path.moveTo(x1, y1);
  9. // 二次贝塞尔曲线
  10. path.quadraticCurveTo(cpx2,cpy2,x2,y2);
  11. bezierLineGroup
  12. .append('path')
  13. .attr('d', path.toString())
  14. .style('fill','none')
  15. .style('stroke','red')
  16. .style('stroke-width','2');

效果如下:

polygonal8.jpg

3.2 三次贝塞尔曲线

基于以上代码,只需要改动 二次贝塞尔曲线为三次贝塞尔曲线就可以了:

  1. ...
  2. // 二次贝塞尔曲线
  3. // path.quadraticCurveTo(cpx2,cpy2,x2,y2);
  4. // 三次贝塞尔曲线
  5. path.bezierCurveTo(cpx1,cpy1,cpx2,cpy2,x2,y2);
  6. ...

效果如下:

polygonal9.jpg

小结

一根小小的线条,效果也有很多。在可视化世界里,组成很多图形的基础就是线条,打好基础,你才能得心应手地使用它来构建你的可视化世界。在贝塞尔曲线部分,我们偷懒直接使用了 D3 API 构造了曲线路径,你也可以尝试用自己的算法去画一条贝塞尔曲线,动手做起来。