学习D3.js(二十)力导向图

770 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

引入D3模块

  • 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>

数据

    // 节点集
    // radius 节点半径
    const nodes = [
      { name: '重庆市', radius: 4 },
      { name: '北碚区', radius: 3 },
      { name: '渝北区', radius: 3 },
      { name: '江北区', radius: 3 },
      { name: '巴南区', radius: 3 },
      { name: '沙坪坝区', radius: 3 },
      { name: '大渡口', radius: 3 },
      { name: '万州区', radius: 3 },
      { name: '天府镇', radius: 2 },
      { name: '水土镇', radius: 2 },
      { name: '蔡家镇', radius: 2 },
      { name: '土场镇', radius: 2 },
      { name: '两江名居', radius: 1 },
      { name: '上城中央', radius: 1 },
      { name: '灯塔', radius: 1 }
    ]
    // 边集
    // value 控制线的长短
    const edges = [
      { source: 0, target: 1, value: 2 },
      { source: 0, target: 2, value: 2 },
      { source: 0, target: 3, value: 2 },
      { source: 0, target: 4, value: 2 },
      { source: 0, target: 5, value: 2 },
      { source: 0, target: 6, value: 2 },
      { source: 0, target: 7, value: 2 },
      { source: 2, target: 8, value: 1.5 },
      { source: 2, target: 9, value: 1.5 },
      { source: 2, target: 10, value: 1.5 },
      { source: 2, target: 11, value: 1.5 },
      { source: 11, target: 12, value: 1 },
      { source: 11, target: 13, value: 1 },
      { source: 11, target: 14, value: 1 }
    ]
  • 边集中的sourcetarget对应节点集中的下标。
  • 当节点集存在id属性时就是对应id属性。

添加画布

  • 初始化画布。
    // 画布
    const width = 1000
    const height = 1000
    const svg = d3
      .select('.d3Chart')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .style('background-color', '#1a3055')
    // 图
    const chart = svg.append('g')

比例尺和配置信息

// 创建颜色比例尺
const colorScale = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, nodes.length + 1))
  • 颜色比例尺,参数相同返回相同颜色。
const force = d3
  .forceSimulation()
  .force('link', d3.forceLink())
  .force('charge', d3.forceManyBody())
  .force('center', d3.forceCenter(width / 2, height / 2))
  1. .forceSimulation() 创建一个力模拟。
  2. .force(name[, force]) 如果指定了力,则为指定的名称分配力并返回此模拟。
  3. d3.forceLink() 适用每个链接的源和目标,链接力。
  4. d3.forceManyBody() 多体力。
  5. d3.forceCenter(x, y) 使用指定的x和y坐标创建新的定心力。如果未指定x和y,则默认为{0,0}。
  • 使用d3.forceSimulation() 创建力模拟函数。
    const forceNodes = force.nodes(nodes).on('tick', ticked)
  1. .nodes() 指定节点,将模拟的节点设置为指定的对象数组。
  2. .on('tick', fun) 监听迭代次数逐步执行模拟。每一次力的模拟都会执行这个方法。
  • 加入节点数据,计算节点布局信息。
  force
      .force('link')
      .links(edges)
      .distance(function (d) {
        //每一边的长度
        return d.value * 150
      })
  1. .force('link') 如果未指定力,则返回具有指定名称的力函数。
  2. .links() 指定链接,设置与此力关联的链接数组,重新计算每个链接的距离和强度参数,并返回此力。
  3. .distance() 指定距离,则将距离访问器设置为指定的数字或函数,重新计算每个链接的距离访问器,并返回此力.
  • 加入边集数据,生成关联线布局信息。

绘制节点和连线

const line = chart.append('g').selectAll().data(edges).enter().append('g')
  • 创建连线绘制组,绑定数据。对每条线创建绘制组,放入连线和文本,后期方便处理。
// 绘制线
const links = line.append('line').attr('stroke', '#ccc').attr('stroke-width', 1)
const linksText = line
  .append('text')
  .text(function (d) {
    return d.value
  })
  .attr('fill', '#ccc')
  • 创建连线和连线文本。这里没有位置信息,因为力模拟会实时变化位置,位置信息都在ticked()函数中设置。
const nodesChart = chart
      .append('g')
      .selectAll()
      .data(nodes)
      .enter()
      .append('g')
      .attr('transform', function (d, i) {
        var cirX = d.x
        var cirY = d.y
        return 'translate(' + cirX + ',' + cirY + ')'
      })
  • 创建节点绘制组。
nodesChart
  .append('circle')
  .attr('r', function (d, i) {
    // 半径
    return d.radius * 10
  })
  .attr('fill', function (d, i) {
    return colorScale(d.name)
  })

nodesChart
  .append('text')
  .attr('x', -20)
  .attr('y', -5)
  .attr('dy', 10)
  .attr('font-size', 12)
  .text(function (d) {
    return d.name
  })
  .attr('fill', '#ccc')
  .attr('pointer-events', 'none')
  .style('user-select', 'none')
  • 节点信息和文本信息在同一绘制组下,方便后期操作。
function ticked() {
  links
    .attr('x1', function (d) {
      return d.source.x
    })
    .attr('y1', function (d) {
      return d.source.y
    })
    .attr('x2', function (d) {
      return d.target.x
    })
    .attr('y2', function (d) {
      return d.target.y
    })

  linksText
    .attr('x', function (d) {
      return (d.source.x + d.target.x) / 2
    })
    .attr('y', function (d) {
      return (d.source.y + d.target.y) / 2
    })

  nodesChart.attr('transform', function (d) {
    return 'translate(' + d.x + ',' + d.y + ')'
  })
}
  • 创建ticked函数,在内部设置图形的位置信息。
function ticked() {
  links
    .attr('x1', function (d) {
      return d.source.x
    })
    .attr('y1', function (d) {
      return d.source.y
    })
    .attr('x2', function (d) {
      return d.target.x
    })
    .attr('y2', function (d) {
      return d.target.y
    })

  linksText
    .attr('x', function (d) {
      return (d.source.x + d.target.x) / 2
    })
    .attr('y', function (d) {
      return (d.source.y + d.target.y) / 2
    })

  nodesChart.attr('transform', function (d) {
    return 'translate(' + d.x + ',' + d.y + ')'
  })
}

1.gif

添加交互

  • 上面关联过多、排斥力未设置,绘制的图形都在一起。添加拖拽交互。
nodesChart.call(d3.drag().on('start', started).on('drag', dragged).on('end', ended))

// d.fx 和 d.fy 表示固定坐标
function started(e, d) {
  force.alphaTarget(0.8).restart() // 设置衰减系数
  d.fx = d.x
  d.fy = d.y
}
function dragged(e, d) {
  d.fx = e.x
  d.fy = e.y
}
function ended(e, d) {
  force.alphaTarget(0).restart()
  d.fx = null
  d.fy = null
}
  1. .alphaTarget(0.8) 对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]。
  2. d.fx、d.fy 节点的固定,设置后节点会移动到对应位置。
  3. d3.drag() 创建一个拖动事件,然后绑定到元素上。
  • 使用d3.drag()创建拖动事件。设置开始拖动时元素为当前位置并重置力函数的衰减系数,拖动中设置为当前鼠标位置,结束时重置固定位置和衰减系数。

2.gif