AboutPostsGithub

D3.js基础使用与简单Network实现

D3.js 基础使用

D3 (or D3.js) is a free, open-source JavaScript library for visualizing data. Its low-level approach built on web standards offers unparalleled flexibility in authoring dynamic, data-driven graphics.

D3是一个开源的可视化JavaScript库,它基于Web标准底层方法在创作动态数据驱动图形方面提供了无与伦比的灵活性。它通过对SVG的节点粒度操作进行数据驱动的图形绘制,一般的操作过程类似于Css,先选中html节点,然后再对节点进行样式赋值。

D3用途广泛,从柱状图到网络图,从遗传图到折现图,它善于图形化有规律的数据,主要应用在图表方面。一般来说,我们会使用D3对SVG进行修改,这要求我们对SVG及它所使用的其他元素有所了解(比如<d><p><rect><circle>等)。

选择器

通常的,我们使用 D3 提供的 d3.select(query) / d3.selectAll(query) 方法来选择 DOM 节点或节点集合,它类似于jQuery,入参 queryCSS 所支持的选择器,如标签选择器、类选择器、ID 选择器、属性值选择器等。

方法 d3.select(query) 会返回符合条件的第一个 DOM 元素,而方法 selectAll(query) 则选择所有符合条件的 DOM 元素,如果没有元素被选中则返回空的选择集。

该方法返回的选择集对象(或数组)具有丰富的方法,如设置样式属性,更改 HTML 或 文本内容,注册事件监听器,添加、移除、排序节点等,这样就可以通过链式调用的方式操作 DOM。

// 操作单个节点
d3.select('body')
  .style('background-color', 'black'); // 修改 <body> 元素的背景色

// 也可以操作节点集合
d3.selectAll('p')
  .style('color', 'white'); // 修改 <p> 元素的字体颜色

在 D3 中大部分情况下,属性值的值设置除了支持传递静态常量值,还支持传递函数,返回动态计算得到的值,例如 D3 的图形模块 shape提供了相关函数,基于数据计算出 <path> 元素的属性 d 的值,用于绘制折线图。

// 设置不同的id
  svg.selectAll("marker")
    .attr("id", (d) => `arrow-${d}`)

数据绑定

d3 通过 data()方法将element与数据绑定,并且定义entry与exit更新数据,默认根据 index 将 DOM 节点(选择集)和数据(以数组的形式列出)一一对应,然后在使用函数设置 DOM 节点样式属性时,分别将对应的数据作为第一个参数 d 传递到设置函数中,动态求出属性值,这样就实现了数据驱动文档 Data-Driven Documents。

注意,这与Vue的双向数据绑定差别巨大,它只是做一次数据映射,并非真正的数据绑定。

元素增删

元素更改

针对这个问题,D3 提出 3 个概念:

  • 如果 DOM 节点多出来,则未绑定数据的节点会进入名为 exiting 选择集(准备从页面「离去」的节点,一般在后续操作中删除)
  • 如果数据元素多出来了,则对应多出来的占位节点(虚拟节点)会进入名为 entering 选择集(准备「进入」页面的节点,一般会在后续操作中实例化这些 DOM 节点,并插入在页面的相应位置)
  • 可与数据对应上的 DOM 节点,进入名为 updating 选择集,它是默认选择集,即 data() 方法返回的对象就是 update 选择集(而 enter 选择集和 exit 选择集需要调用该对象的 enter()exit() 方法才能获得)

在绑定数据后,D3 没有立即更新(增删)页面节点,而是生成 3 个选择集,这样为数据可视化提供了更大的灵活度和可定制性,例如对于 exiting 选择集的节点,可以在删除时设置一些淡出的动效;对于 entering 选择集的节点可以设置不一样的颜色,高亮出来它们是新增到页面上的。

D3 Api join()可以自动处理三个集合,一个可行的做法是选中数据之后直接join。

d3.selectAll('circle')
    .data(data)
    .join('circle')   // 返回 entering 和 updating 选择集的合并集

如果希望对 exiting 选择集、entering 选择集或 updating 选择集分别进行操作,可以在方法 join 中传入函数。

d3.selectAll('circle')
  .data(newData, d => d)
  .join(
    // 第一个传递的函数入参是 entering 选择集
    enter => {
      //  entering selection handler
      // 最后需要返回 entering 选择集实例化的节点,以便 join 方法最后将它以 updating 选择集进行合并
      return enter.append('circle')
    },
    // 第二个传递的函数入参是 updating 选择集
    update => {
        // updating selection handler
        update.attr("fill", "blue")
    }
    exit => {
      // exiting selection handler
      exit.remove() // 将 exiting 选择集对应的节点从页面删除
    })

Network

svg可以预先放入maker元素,来定义某些next的特殊形状

<svg style="border: 1px solid #000">
  <defs>
    <marker
      id="end"
      viewBox="0 0 10 10"
      refX="15"
      refY="5"
      markerUnits="strokeWidth"
      markerWidth="5"
      markerHeight="5"
      orient="auto"
    >
      <path d="M 0 0 L 10 5 L 0 10 z" fill="#fff000" />
    </marker>
  </defs>
</svg>

JavaScript部分代码如下:

import * as d3 from "d3";
// 向心力模拟
  const simulation = d3
    .forceSimulation(nodes)
    .force(
      "link",
      d3.forceLink(links).id((d) => d.id)
    )
    .force("charge", d3.forceManyBody().strength(-500));
// 选中svg
  const svg = d3
    .select("svg")
    .attr("viewBox", [-700 / 2, -400 / 2, 700, 400])
    .style("font", "12px sans-serif");
 // Per-type markers, as they don't inherit styles.
 svg
    .append("defs")
    .selectAll("marker")
    .join("marker")
    .attr("id", (d) => `arrow-${d}`)
    .attr("viewBox", "0 -50 10 10")
    .attr("refX", 15)
    .attr("refY", -0.5)
    .attr("markerWidth", 60)
    .attr("markerHeight", 60)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M0,-5L10,0L0,5");

// network的link实例化并绑定数据
  const link = svg
    .append("g")
    .attr("fill", "none")
    .attr("stroke-width", 1.5)
    .selectAll("path")
    .data(links)
    .join("path")
    .attr("stroke", "#fff000")
    .attr("marker-end", 'url("#end")');

  const node = svg
    .append("g")
    .attr("fill", "#ada307")
    .attr("stroke-linecap", "round")
    .attr("stroke-linejoin", "round")
    .selectAll("g")
    .data(nodes)
    .join("g")
    .call(drag(simulation));

  simulation.on("tick", () => {
    link.attr("d", linkArc);
    node.attr("transform", (d) => `translate(${d.x},${d.y})`);
  });

zoom 方法,缩放svg

function setZoom(svg) {
  let zoom = d3
    .zoom()
    .scaleExtent([0.1, 10]) // 鼠标缩放的距离, 范围
    .on("start", () => {
      // zoom 事件发生前 将变小手
      console.log("start");
      svg.style("cursor", "pointer");
    })
    .on("zoom", (e) => {
      svg
        .selectAll("svg>g")
        .attr(
          "transform",
          "translate(" + e.transform.x + "," + e.transform.y + ") scale(" + e.transform.k + ")"
        );
    })
    .on("end", () => {
      svg.style("cursor", "default");
    });
  svg.call(zoom).call(zoom.transform, d3.zoomIdentity.scale(1));
}