初识 D3.js :入门教程 + 与echarts对比

621 阅读7分钟

前言

在做Node Library依赖关系分析工具时,需要用可视化来实现依赖之间的关系,暂时决定使用d3.js的力有向图,后续可能会修改。故,学习了d3的一些基础的语法,将其和echarts可视化进行了对比。如果小伙伴们感兴趣,可以仔细看看。

✨ 为什么选择D3.js

➡ d3 优点

  • 数据驱动视图 --将数据与DOM元素绑定,根据数据动态地生成图表和可视化效果
  • 代码十分简洁
  • 擅长矢量图形,缩放不损失图形精度,不擅长位图和瓦片,不擅长探索型可视化;

➡ 什么时候选择D3 ?什么时候选择echarts?

  • 对于客户需求要求的图表拥有大量的用户交互场景,用 d3 比较方便,因为 d3 中 svg 画图支持事件处理器,他是基于 dom 进行操作的。
  • 对于大量的数据展示并且对于用户交互场景没什么要求,就只是展示数据,那我建议使用 echarts,如果使用 d3 的话展示的每一个数据都是一个标签,那么当数据发生改变的时候这时候图表会重新渲染,会不停的操作 dom,操作 dom 是很耗费性能的。【简单版本:D3操作DOM,没什么交互的话,选择echarts】
  • 从兼容性方面考虑:echarts 兼容到 IE6 及以上的所有主流浏览器,而 d3 兼容 IE9 及以上以及所有的主流浏览器,如果项目考虑兼容 IE6,建议使用 echarts。

✨ d3基础语法

1️⃣ 总结一下 D3 可视化的基本步骤如下:

  • 创建新元素并绑定数据(html的元素可理解为划定区域和声明类型的闭合标签,如p表示其是一个段落,是段落就可以有段落文本、长宽、id等属性和标识);
  • 设置相应元素的可视属性,将数据值映射为元素大小、颜色、位置等可视属性;
  • 对元素进行排列和变换,还有响应交互;

2️⃣ Svg 基础知识了解

SVG(Scalable Vector Graphics,可伸缩矢量图形)基于XML标签来表示图形。基于HTML文档的可视化基本都使用canvas或svg元素作为数据到图形的映射容器。D3也可以直接操作div或其他原生HTML元素来绘图,但这就显得笨重不灵活,且容易出现浏览器间不一致的问题。而用 SVG就更可靠,图形效果更一致,且绘图速度更快。

SVG 元素可以理解为能在上面绘制各种形状的画布。

简单一句话概括: svg 是一个可伸缩矢量图形,缩放不损失图形精度

3️⃣ 具体应用

🟠 元素定位和数据绑定

简单介绍D3如何选择及增删文档对象的方法以及数据绑定的方法

  • 元素定位

    🔸 选择元素

      select() 方法 :用于选择单个元素的常用方法。

      示例: 假设 HTML 中有如下的 DOM 结构:

     <div id="example">
       <p>Hello, D3.js!</p>
     </div>
     
     // 你可以使用 D3.js 的 `select()` 方法来选择 `<p>` 元素,并对其进行操作:
    
     // 选择 <p> 元素
     const pElement = d3.select("p");
    
     // 对选中的 <p> 元素进行样式设置
     pElement.style("color", "blue");
     pElement.style("font-size", "20px");
    
    • d3.select("body") ; //选择HTML里的body元素;
    • d3.select("#apple"); //选择id为apple的元素;例如会匹配上<p id="apple">一段文本</p>
    • d3.select(".apple"); //选择class为apple的元素;会匹配上<p class="apple">一段文本</p>
    •   如果想获得所有满足条件的元素,用selectAll()方法,写法和上面一致,把select变成selectAll
     // react
     const LinePlot = (props) => {
     const svgRef = useRef(null);
     const gdefs = d3.create("svg:defs");
     const svg = d3.select(svgRef.current)
     svg.append(() => gdefs.node());
     gdefs.selectAll("marker")
    
      return (
         <div >
           {/* 将 SVG 元素插入到 DOM 中 */}
           <svg ref={svgRef} >
           </svg>
         </div>
       )
     }
    

    🔸添加元素

      append() 方法:添加元素

         // 创建一个 SVG 元素
         const svg = d3.create("svg")
         // 在html文档里的<svg>标签下从无到有地增加了一个<rect></rect>元素;
         svg.append("rect")
     ```
    
    
  • 设定属性

      🔸 选定或添加我们需要操纵的元素后,便可以编辑元素的属性,例如图形位置、填充色、标识等属性。通过.attr(name,value)给所选元素添加属性,name是属性名称,value是属性值。

           const svg = d3.select(svgRef.current)
             .attr("viewBox", [-width / 2, -height / 2, width, height])
             .attr("width", width)
             .attr("height", height)
             .attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;")
    
  • 数据绑定

      🔸 通过data(vals[,key])绑定数组vals中的每一项到选中的元素,key是一个用于指定绑定规则的函数。

  gdefs.selectAll("marker")
        . data (types) // 使用 data() 方法绑定数据
        .join("marker") // 根据数据绑定状态对元素进行操作
        .attr("id", d => `arrow-${d}`)
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 15)
        .attr("refY", -0.5)
        .attr("markerWidth", 6)
        .attr("markerHeight", 6)
        .attr("orient", "auto") // 定义图形在路径上的朝向: auto-浏览器会自动根据路径的朝向来调整图形的朝向。
        .append("path") // append 添加 用于绘制路径,例如曲线、线段等。主要用于箭头
        .attr("fill", color)
        .attr("d", "M0,-5L10,0L0,5");
  • 读取本地数据

        🔸  d3.csv("food.csv", function(data) {dataset=data;})可以读取本地的csv文件数据进行使用

     d3.csv("bar-data.csv", function(dataset) {
           d3.select("body").selectAll("svg")
                      .data(dataset)
                      .enter()
                      .append("svg")
                      .style("background-color","#1EAFAE")
                      .attr("width",50)
                      .attr("height",function(d){return d*10 +"px";});
     });
    

🟡 SVG预定义元素

  🔸 以下是 SVG 中的一些常见预定义元素:

预定义元素定义
<rect>矩形元素,用于绘制矩形。可以设置矩形的位置、宽度、高度以及圆角属性。
<circle>圆形元素,用于绘制圆形。可以设置圆心的坐标和半径。
<ellipse>椭圆元素,用于绘制椭圆。可以设置椭圆的中心坐标和水平、垂直半径。
<line>直线元素,用于绘制直线。可以设置直线的起始点和终点坐标。
<polyline>折线元素,用于绘制由多个连接的线段组成的折线。
<polygon>多边形元素,用于绘制封闭的多边形。与折线类似,但是多边形的最后一个点会与第一个点相连,形成封闭的形状。
<path>路径元素,用于通过路径命令绘制任意复杂的图形。路径命令包括直线、曲线、弧线等。
<text>分组元素,用于将多个 SVG 元素组合在一起,并对这个组进行统一的变换、样式设置和事件绑定。
<marker>容器元素,它可以包含其他 SVG 元素,如 <path><circle><rect> 等,用于定义标记的图形样式。

  🔸 一些通用属性:

通用属性
fill表示要填充的颜色
stroke表示边框的颜色
stroke-width边框宽度
opacity透明度。数值在[0,1]之间,值越小越透明
💠 <rect> 元素

属性:

  • x:矩形左上角的x坐标,注意是左上角不是中心点的坐标;
  • y:矩形左上角的y坐标;
  • width:矩形宽度;
  • height:矩形高;
  • rx:缺省是正常矩形,否则是圆角矩形,圆角处椭圆在x方向的半径;
  • ry:圆角矩形的圆角在y方向的半径;
💠<circle>元素

属性:

  • cx:圆心的x坐标,可以记为circle-x;
  • cy:圆心的y坐标;
  • r:圆的半径;
💠 <ellipse>元素

属性:

  • cx:椭圆圆心的x坐标;
  • cy:圆心的y坐标;
  • rx:椭圆的水平半径;
  • ry:椭圆的垂直半径;
💠<line>元素

属性:

  • x1:起点的x坐标;
  • y1:起点的y坐标;
  • x2:终点的x坐标;
  • y2:终点的y坐标;
💠<path>元素

<path>的写法是:给出一个坐标点,在坐标点前面添加一个英文字母,用于标识如何运动到此坐标点。英文字母按照功能可分为五类。大写表示绝对坐标,小写表示相对坐标。

分类:

  • 移动类: M即moveto,表示将画笔移动到指定坐标,不绘制移动的路径;
  • 直线类:L,画直线到指定坐标(lineto);H,画水平直线到指定坐标(horizontal lineto);V,画垂直直线到指定坐标(vertical lineto);
  • 曲线类:C,curveto,画三次贝塞尔曲线经两个指定控制点到达终点坐标;S,smooth curveto,与前一条三次贝塞尔曲线相连,第一个控制点为前一条曲线第二个控制点的对称点,只需输入第二个控制点和终点,即可绘制一个三次贝塞尔曲线;Q,quadratic Bézier curveto,画二次贝塞尔曲线经一个指定控制点到达终点坐标;T,Shorthand/smooth quadratic Bézier curveto,与前一条二次贝塞尔曲线相连,控制点为前一条二次贝塞尔曲线控制点的对称点,只需输入终点,即可绘制一个二次贝塞尔曲线;
  • 弧线类:A,elliptical arc,画椭圆曲线到指定坐标;
  • 闭合类:Z,closepath,绘制一条直线连接终点和起点,用来封闭图形;
      // 创建 <g> 元素并添加链接(<path>)
      const link = gLink
        .attr("fill", "none")
        .attr("stroke-width", 1.5)
        .selectAll("path")
        .data(graph)// 使用 data() 方法绑定数据
        .join("path")// 使用 join() 方法来创建 <path> 元素
        .attr("stroke", d => color(d.type))
        .attr("marker-end", d => `url(${new URL(`#arrow-${d.type}`, location)})`)
      // 节点拖动时,路径重新指向
        function linkArc(d) {
        const r = Math.hypot(d.target.x - d.source.x, d.target.y - d.source.y);
        return `
            M${d.source.x},${d.source.y}
            A${r},${r} 0 0,1 ${d.target.x},${d.target.y}
          `;
      }
        simulation.on("tick", () => {
        link.attr("d", linkArc);
        node.attr("transform", d => `translate(${d.x},${d.y})`);
      });
💠<text>元素

属性:

  • x:文字位置的x坐标。
  • y:文字位置的y坐标。
  • dx:相对于当前位置在x方向上平移的距离(值为正则往右,负则往左);
  • dy:相对于当前位置在y方向上平移的距离(值为正则往下,负则往上)。
  • textLength:文字的显示长度(不足则拉长,足则压缩)
  • rotate:旋转角度(顺时针为正,逆时针为负)
      const text = node.append("text")
        .attr("x", 8)
        .attr("y", "1.5em")
        .attr('text-anchor', 'middle')
        .text(d => d.id)
        .clone(true).lower()
        .attr("fill", "none")
        .attr("stroke", "white")
        .attr("stroke-width", 3);
💠 <g> 元素

<g> 元素表示 SVG 中的分组元素(Group Element)。<g> 元素用于将多个 SVG 元素组合在一起,形成一个组,并且可以对这个组进行统一的变换、样式设置和事件绑定。

 // 此处应该是代表 link 即点与点之间的连接
 // 创建 <g> 元素并添加链接(<path>)
      const link = gLink
        .attr("fill", "none")
        .attr("stroke-width", 1.5)
        .selectAll("path")
        .data(graph)// 使用 data() 方法绑定数据
        .join("path")// 使用 join() 方法来创建 <path> 元素
        .attr("stroke", d => color(d.type))
        .attr("marker-end", d => `url(${new URL(`#arrow-${d.type}`, location)})`)
💠<marker> 元素

SVG中的一个特殊元素,它用于定义可重复使用的箭头、标记或其他形状,标记贴附于path、line、polyline、 polygon元素上。(目前使用里面是箭头)

在SVG中,<marker> 元素通过定义一个可插入的图形,然后可以在路径元素的marker-startmarker-midmarker-end属性中引用,用于显示在路径的起点、中点和终点位置。

属性:

  • id: 为 <marker> 元素定义一个唯一的ID,以便后续在路径元素中引用。
  • viewBox: 定义 <marker> 元素的视口框,用于指定图形的可视区域。
  • refXrefY: 指定图形在视口框中的参考点(坐标),用于定位图形在路径上的位置。
  • markerWidthmarkerHeight: 定义 <marker> 元素的宽度和高度。
  • orient: 定义图形的方向,通常使用 "auto" 表示自动根据路径的朝向来调整图形的方向。
 gdefs.selectAll("marker")
        .data(types) // 使用 data() 方法绑定数据
        .join("marker") // 根据数据绑定状态对元素进行操作
        .attr("id", d => `arrow-${d}`)
        .attr("viewBox", "0 -5 10 10")
        .attr("refX", 15)
        .attr("refY", -0.5)
        .attr("markerWidth", 6)
        .attr("markerHeight", 6)
        .attr("orient", "auto") // 定义图形在路径上的朝向: auto-浏览器会自动根据路径的朝向来调整图形的朝向。
        .append("path") // append 添加 用于绘制路径,例如曲线、线段等。主要用于箭头
        .attr("fill", color)
        .attr("d", "M0,-5L10,0L0,5");

🟢 图表交互

事件监听器

D3的选择集有一个方法on(),用来设定事件的监听器。

      const node = gNode
        .attr("fill", "currentColor")
        .attr("stroke-linecap", "round")
        .attr("stroke-linejoin", "round")
        .selectAll("g")
        .data(nodeArray)
        .join("g")
        .call(drag(simulation))
       // 事件监听器 :mouseover 事件名称 ,handleMouseOver 监听器函数
. on ( 'mouseover' , handleMouseOver) // 鼠标移动到节点上
. on ( 'click' , handleMouseOut) // 点击节点
       function  handleMouseOut ( event,d ) {
 // event 当前事件 d 获取的节点对象
 var hoveredNode = d; // 获取当前鼠标悬停的节点数据
 // 设置节点的透明度
node. style ( 'opacity' , 1 );
 // 设置连接线的透明度
link. style ( 'opacity' , 1 );
} 
键鼠事件
🔺 鼠标事件:
  • click:单击事件,鼠标单击某个元素触发,相当于mousedown和mouseup组合在一起;
  • dblclick:鼠标双击事件;
  • mouseover:鼠标的光标放在某元素上(悬停在元素上);
  • mouseout:光标从某元素上移出来时;
  • mousedown:鼠标按钮被按下;
  • mouseup:鼠标按钮被松开;
🔻 键盘事件:
  • keydown:当用户按下任意键时触发,按住不放会重复触发此事件,这一事件不会区分字母的大小写,例如“A”和“a”被视为一致;
  • keypress:当用户按下字符键(大小写字母、数字、加号、等号、回车等)时触发,按住不放会重复触发此事件,该事件就会区分字母的大小写;
  • keyup:当用户松开按键时触发,该事件不区分字母的大小写;

keydown和keypress事件的区别在于keydown用于任意键的事件,而keypress用于字符键,如果只需要处理字母数字类的响应,或是要对大小写字母分别处理的时候,使用keypress;如果要处理上下左右(↑→)、Shift、Ctrl等特殊键的输入,使用keydown。

缩放

缩放事件:d3.zoom().on("zoom", zoomed)

dragzoom一般通过call调用

      const zoom = d3.zoom()
        .scaleExtent([1, 10]) // 设置缩放范围,1 表示原始大小,10 表示最大放大为原始大小的10倍
        .on("zoom", (d3) => zoomed(d3));
      const svg = d3.select(svgRef.current)
        .attr("viewBox", [-width / 2, -height / 2, width, height])
        .attr("width", width)
        .attr("height", height)
        .attr("style", "max-width: 100%; height: auto; font: 12px sans-serif;")
        . call (zoom)
       // 定义缩放函数的回调
      function zoomed(event) {
      // 获取缩放和平移的变换
      const transform = event.transform;
      link.attr('transform', transform);
      node_.attr('transform', transform)
      }

悬停文本标签

过渡动画

过渡动画同样通过事件监听和缓动实现过渡效果和数据更新,实现友好的交互;

.transition()transition()默认情况延迟(delay)为0ms,持续时长(duration)为250ms,可以自行设置这两个参数。

与HTML元素交互

D3作为一个JavaScript库,自然可以和原生的HTML元素进行交互,例如响应按钮的点击事件,在html中配置了按钮和点击监测,<button type="button" onclick="update()"> 更新 </button>,点击按钮触发事件,在函数update里面调用d3的绘制代码,实现交互。

状态条是很实用的元素,通过状态条调节d3图表的参数,例如下面通过状态条调节绘制矩形的填充颜色,给状态条添加了onchange的事件监听器,有变化时更新矩形的颜色。

//html中需要额外引入 <script src="https://unpkg.com/d3-simple-slider"></script>
var num2hex = rgb => {
    return rgb.map(color => {
        var s = color.toString(16);
        if (s.length === 1) {
          s= '0' + s;
        }
        return s;
      }).join('');
  };

var rgb = [0,175,174];
var colors = ['red', 'green', 'blue'];

var svg = d3.select('div#d3-slider').append('svg')
    .attr('width', 600).attr('height', 400).append('g')
    .attr('transform', 'translate(30,30)');
var box = svg
    .append('rect')
    .attr('width', 160)
    .attr('height', 100)
    .attr('transform', 'translate(400,30)')
    .attr('fill', `#${num2hex(rgb)}`);

rgb.forEach((color, i) => {
    var slider = d3
      .sliderBottom()
      .min(0)
      .max(255)
      .step(1)
      .width(300)
      .default(rgb[i])
      .displayValue(false)
      .fill(colors[i])
      .on('onchange', num => {
        rgb[i] = num;
        box.attr('fill', `#${num2hex(rgb)}`);
        d3.select('p#value-color').text(`#${num2hex(rgb)}`);
      });
    svg.append('g').attr('transform', `translate(30,${60 * i})`).call(slider);
});
d3.select('p#value-color').text(`#${num2hex(rgb)}`);

✨ 参考资料:

1、官方文档网站d3js.org.cn/

2、D3库实践笔记之图表交互 |可视化系列 lynsdata.cn/2020/10/03/…