preface

This article was inspired by a demo page by Mike Bostock

The original demo was developed based on D3.js v3. The author rewrote it using D3.js V5 and changed it to use ES6 syntax.

Source: making

Online demo: Demo

The effect

As you can see, there is a legend in the upper left corner and a patent relationship between various mobile phone companies in the middle.

There are three lines in the legend:

  • Solid red line: patent litigation in progress (arrow points to litigant)
  • Blue dotted line: litigation is over
  • Solid green line: Patent granted

implementation

Let’s take a step-by-step look at how to achieve the effect shown above.

Analyze the data

[{source: 'Microsoft'.target: 'Amazon'.type: 'licensing' },
  { source: 'Microsoft'.target: 'HTC'.type: 'licensing' },
  { source: 'Samsung'.target: 'Apple'.type: 'suit' },
  { source: 'Motorola'.target: 'Apple'.type: 'suit' },
  { source: 'Nokia'.target: 'Apple'.type: 'resolved' },
  { source: 'HTC'.target: 'Apple'.type: 'suit' },
  { source: 'Kodak'.target: 'Apple'.type: 'suit' },
  { source: 'Microsoft'.target: 'Barnes & Noble'.type: 'suit' },
  { source: 'Microsoft'.target: 'Foxconn'.type: 'suit'},... ]Copy the code

As you can see, each piece of data consists of the following parts:

  • source: Company name of the litigant
  • target: Company name of the sued party
  • type: Current litigation status

It should be noted that some companies (such as Apple and Microsoft) are involved in multiple lawsuits at the same time, but we only assign a node to each company in data visualization, and then use a link to represent the relationship between each company.

The most important thing for data visualization is the mapping relationship between data and images. In this example, our visualization logic is as follows:

  • Consider each company as a circular node in the diagram
  • Each litigation relation is represented as a line between two circular nodes

Company ==> circular node

Litigation relationship ==> Connection

Technical analysis

This demo uses d3-force and D3-Drag in D3.js, as well as the basic D3-Selection.

(For the convenience of building the user interface, Vue is used as the front-end framework. But Vue doesn’t affect the data visualization logic, and not using it doesn’t affect our implementation.)

Code implementation

Now let’s get into the code. First let’s draw the circular nodes that each company represents:

As mentioned above, in the original data, some companies appear in different litigation relationships several times, and we need to draw a unique node for each company, so we need to do some processing on the data:

  initData() {
    this.links = [
      { source: 'Microsoft'.target: 'Amazon'.type: 'licensing' },
      { source: 'Microsoft'.target: 'HTC'.type: 'licensing' },
      { source: 'Samsung'.target: 'Apple'.type: 'suit' },
      { source: 'Motorola'.target: 'Apple'.type: 'suit' },
      { source: 'Nokia'.target: 'Apple'.type: 'resolved'},... ]// Some data is omitted here

    this.nodes = {}

    // Compute the distinct nodes from the links.
    this.links.forEach(link= > {
      link.source =
        this.nodes[link.source] ||
        (this.nodes[link.source] = { name: link.source })
      link.target =
        this.nodes[link.target] ||
        (this.nodes[link.target] = { name: link.target })
    })
    console.log(this.links)
  }
Copy the code

The logic of the above code is to iterate through all links and place the source and target as keys in nodes, so we get data nodes without duplicate nodes:

As the careful reader may have noticed, there are a lot of x and y coordinates in the above data. Where does this data come from? The answer is D3-force. Because what we want to achieve is to simulate the distribution of physical forces, we use D3-force to simulate and help us calculate the position of each node. The call method is as follows:

this.force = this.d3
  .forceSimulation(this.d3.values(this.nodes))
  .force('charge'.this.d3.forceManyBody().strength(50))
  .force('collide'.this.d3.forceCollide().radius(50))
  .force('link', forceLink)
  .force(
    'center'.this.d3
      .forceCenter()
      .x(width / 2)
      .y(height / 2)
  )
  .on('tick', () = > {if (this.path) {
      this.path.attr('d'.this.linkArc)
      this.circle.attr('transform', transform)
      this.text.attr('transform', transform)
    }
  })
Copy the code

Here we add three forces to d3-force:

  • .force('charge', this.d3.forceManyBody().strength(50))Add attraction to each node
  • .force('collide', this.d3.forceCollide().radius(50))Add a rigid-body collision effect for each node
  • .force('link', forceLink)Add a relay between nodes

After executing the above code, d3-force calculates the coordinates for each node and assigns them to each node as x, Y attributes.

Draw the company’sCircular node

With the data processed, let’s map it to the SVG ==> circle element on the page:

this.circle = this.svgNode // svgNode is the SVG node in the page (d3.select(' SVG '))
  .append('g')
  .selectAll('circle')
  .data(this.d3.values(this.nodes)) // d3.values() convert Object{} to Array[]
  .enter()
  .append('circle')
  .attr('r'.10)
  .style('cursor'.'pointer')
  .call(this.enableDragFunc())
Copy the code

Note here that we call.call(this.enableDragFunc()) at the end of the call. This code is used to implement drag and drop functions for the circle node, which we’ll explain later.

Map nodes to a circle element and set the circle element’s attributes:

  • Radius 10
  • Mouse hover icon is finger
  • Give the x, y attributes of each node to the x, y attributes of circle (˙). This step is not declared in the code, because d3 defaults to the x, y attributes of the data as the x, y attributes of circle.

The result of executing the above code:

Draw the name of the company

After drawing the circular nodes representing the company, it’s easy to draw the company name. You just have to shift the x and y coordinates a little bit.

Here we put the company name to the right of the circular node:

this.text = this.svgNode
  .append('g')
  .selectAll('text')
  .data(this.d3.values(this.nodes))
  .enter()
  .append('text')
  .attr('x'.12)
  .attr('y'.'.31em')
  .text(d= > d.name)
Copy the code

The above code simply places the text element at position (12, 0). We offset the position of the text element during each tick cycle of d3-force so that the text element is 12 pixels to the right of the circle element:

this.force = this.d3
      ...
      .on('tick', () = > {if (this.path) {
          this.path.attr('d'.this.linkArc)
          this.circle.attr('transform', transform)
          this.text.attr('transform', transform)
        }
      })
Copy the code

The effect is as follows:

Draw a line of litigation

Next we join the nodes that have litigation relationships. Since wiring is not a regular graph, we use the PATH element of SVG to implement it.

this.path = this.svgNode
  .append('g')
  .selectAll('path')
  .data(this.links)
  .enter()
  .append('path')
  .attr('class'.function(d) {
    return 'link ' + d.type
  })
  .attr('marker-end'.function(d) {
    return 'url(#' + d.type + ') '
  })
Copy the code

We use ‘link ‘+ D. type to assign different classes to different lawsuit connections, and then add different styles (red solid line, blue dotted line, green solid line) to different class connections through CSS.

The d attribute of path is also set in the TICK cycle of d3-force:

this.force = this.d3
      ...
      .on('tick', () = > {if (this.path) {
          this.path.attr('d'.this.linkArc)
          this.circle.attr('transform', transform)
          this.text.attr('transform', transform)
        }
      })

  linkArc(d) {
    const dx = d.target.x - d.source.x
    const dy = d.target.y - d.source.y
    const dr = Math.sqrt(dx * dx + dy * dy)
    return (
      'M' +
      d.source.x +
      ', ' +
      d.source.y +
      'A' +
      dr +
      ', ' +
      dr +
      ' 0 0,1 ' +
      d.target.x +
      ', ' +
      d.target.y
    )
  }
Copy the code

Here we concatenate a small SVG instruction directly with strings to draw an arc. After completing the above code, we have the following effect:

Add a legend

Now we have basically completed the expected effect, but the picture lacks legend, visitors will not understand the different colors of the curves represent the meaning, so we added legend in the top left corner of the picture.

The implementation method of legend is roughly the same as the above steps, but there are two differences:

  • The legend is fixed in the upper left corner of the screen, and the coordinates can be written directly in the code
  • Legends have one more element than real data: descriptive text

Let’s construct the data of the legend:

const sampleData = [
  {
    source: { name: 'Nokia'.x: xIndex, y: yIndex },
    target: { name: 'Qualcomm'.x: xIndex + 100.y: yIndex },
    title: 'Still in suit:'.type: 'suit'
  },
  {
    source: { name: 'Qualcomm'.x: xIndex, y: yIndex + 100 },
    target: { name: 'Nokia'.x: xIndex + 100.y: yIndex + 100 },
    title: 'Already resolved:'.type: 'resolved'
  },
  {
    source: { name: 'Microsoft'.x: xIndex, y: yIndex + 200 },
    target: { name: 'Amazon'.x: xIndex + 100.y: yIndex + 200 },
    title: 'Locensing now:'.type: 'licensing'}]const nodes = {}
sampleData.forEach((link, index) = > {
  nodes[link.source.name + index] = link.source
  nodes[link.target.name + index] = link.target
})
Copy the code

Following the same steps, let’s draw the legend:

sampleContainer
  .selectAll('path')
  .data(sampleData)
  .enter()
  .append('path')
  .attr('class', d => 'link ' + d.type)
  .attr('marker-end', d => 'url(#' + d.type + ') ')
  .attr('d'.this.linkArc)

sampleContainer
  .selectAll('circle')
  .data(this.d3.values(nodes))
  .enter()
  .append('circle')
  .attr('r'.10)
  .style('cursor'.'pointer')
  .attr('transform', d => `translate(${d.x}.${d.y}) `)

sampleContainer
  .selectAll('.companyTitle')
  .data(this.d3.values(nodes))
  .enter()
  .append('text')
  .style('text-anchor'.'middle')
  .attr('x', d => d.x)
  .attr('y', d => d.y + 24)
  .text(d= > d.name)

sampleContainer
  .selectAll('.title')
  .data(sampleData)
  .enter()
  .append('text')
  .attr('class'.'msg-title')
  .style('text-anchor'.'end')
  .attr('x', d => d.source.x - 30)
  .attr('y', d => d.source.y + 5)
  .text(d= > d.title)
Copy the code

End result:

conclusion

Using D3.js for such data visualization is very simple and flexible. However, d3-force requires a bit of adjustment to achieve the desired effect. The actual implementation code is not very long, and the logic code is in this file: graphGenerator.js. If you are interested, you can read the source code directly.

Would like to continue learning about D3.js

Here is my Github address for d3.js, data visualization blog, and welcome to Start & fork :tada:

D3-blog

If you like it, check out the link below:)

github: ssthouse

Zhihu: Data Visualization/Data Visualization

The Denver nuggets: ssthouse

Welcome to follow my official account: