chartexample.com
  • About
  • Blog
  • Charts

Network chart D3 example

  • Home
  • Charts
  • Network
  • Network chart D3 example
function makeNetwork() {
  //data set
  const data = [
    {
      id: 'item1',
      connections: ['item2', 'item3'],
    },
    {
      id: 'item2',
      connections: ['item4'],
    },
    {
      id: 'item3',
      connections: ['item2', 'item1'],
    },
    {
      id: 'item4',
      connections: ['item1'],
    },
    {
      id: 'item5',
      connections: ['item1', 'item4'],
    },
    {
      id: 'item6',
      connections: ['item1'],
    },
  ]

  // selecting root element for plot
  const svg = d3.select('#chart')

  // getting root element width
  const width = parseInt(svg.style('width')) 
  // getting root element height
  const height = parseInt(svg.style('height')) 
  const padding = 70
  const strokeColor = '#d0d4fc'
  const fillColor = '#285674'

  // generating network datastructure from the data
  // by using custom function
  const structure = getStructure(data)
  // applying network layout to structure items
  applyRandomLayout(structure)
  // getting all nodes data
  const items = getNodes(structure)
  // getting all links data between network nodes
  const links = getLinks(structure)

  // creating linear scaling for Y
  const scaleY = d3.scaleLinear()
    .domain([d3.min(items, d => d.y), d3.max(items, d => d.y)])
    .range([padding, height - padding]) 
  // creating linear scaling for X
  const scaleX = d3.scaleLinear()
    .domain([d3.min(items, d => d.x), d3.max(items, d => d.x)])
    .range([padding, width - padding]) 

  // creating drag behaviour
  const dragHandler = d3.drag()
    // setting general drag callback
    .on('drag', function (e, d) {
      // getting mouse pointer coords relatiely svg
      const [x, y] = d3.pointer(e, svg.node())
      // updating coords directly in data object (d param)
      d.x = scaleX.invert(x)
      d.y = scaleY.invert(y)

      // calling update function to redraw the chart
      update()
    })

  // creating wrapper for all chart components
  const wrapper = svg.append('g')

  // render connections between nodes
  // creating empty selection
  wrapper.selectAll('line')
    // applying connections data
    .data(links)
    // applying element-data connections and creating line elements
    .join('line')
    // applying line coords
    .attr('x1', d => scaleX(d[0].x))
    .attr('y1', d => scaleY(d[0].y))
    .attr('x2', d => scaleX(d[1].x))
    .attr('y2', d => scaleY(d[1].y))
    // applying lines color
    .attr('stroke', strokeColor)

  // render nodes
  // creating empty collection
  const nodes = wrapper.selectAll('g')
    //  applying nodes data
    .data(items)
    // applying element-data connections and creating g elements
    // g element used because each node contain circle and text
    // so its a wrapper
    .join('g')
    // setting node element class
    // for future selections
    .attr('class', 'node')

  // appending circles to each node
  nodes.append('circle')
    // applying circle coords
    .attr('cx', d => scaleX(d.x))
    .attr('cy', d => scaleY(d.y))
    // applying circle radius
    .attr('r', '10')
    // applying each circle color
    .attr('fill', fillColor)
    .attr('stroke', strokeColor)
    // setting cursor style
    .style('cursor', 'pointer')

  // applying text for each node
  nodes.append('text')
    // setting text value
    .text(d => d.data.id)
    // applying text color
    .attr('fill', 'currentColor')
    // setting popup text element coords
    // +15 is a slight correction for better visibility
    .attr('x', d => scaleX(d.x) + 15)
    .attr('y', d => scaleY(d.y) + 15)
    

  // applying drag behavious to selection of all circle elements
  // which represents nodes
  dragHandler(svg.selectAll('circle'))

  // function which create structure with linked object
  // from flat data array
  function getStructure(data) {
    const result = {}
    // getting initial items structure
    data.forEach((item, i) => {
      result[item.id] = {
        data: item
      }
    })
    // setting connections
    data.forEach(item => {
      const connections = item.connections.map(item => result[item])
      result[item.id].connections = connections
    })

    return result
  }

  // function that applies random layout (without directions)
  //  to network structure 
  function applyRandomLayout(data) {
    // spacing between nodes
    const spacing = 100

    // applying coords for each element of structure
    Object.values(data).forEach((item, i) => {
      // getting previous generated node object 
      // or default object with coords for the first one
      const prev = i > 0 ? Object.values(data)[i - 1] : {x: 0, y: 0}
      // generating node coords
      item.x = prev.x + spacing
      item.y = (prev.y + spacing) * Math.sin(i)
    })
  }

  // function that returns array of node objects
  function getNodes(layout) {
    return Object.values(layout).map(node => node)
  }

  // function that returns array of arrays which represents
  // connections between two nodes
  function getLinks(layout) {
    const links = []
    Object.values(layout).forEach(node => {
      node.connections.forEach(connection => {
        links.push([node, connection])
      })
    })
  
    return links
  }

  // general update callback
  function update() {
    // generating updated links and nodes data from structure
    const links = getLinks(structure)
    const items = getNodes(structure)
   
    // render connections 
    const nodes = wrapper.selectAll('.node')
      .data(items)
      .join('g')
    
    wrapper.selectAll('line')
      .data(links)
      .join()
      .attr('x1', d => scaleX(d[0].x))
      .attr('y1', d => scaleY(d[0].y))
      .attr('x2', d => scaleX(d[1].x))
      .attr('y2', d => scaleY(d[1].y))

    // render nodes
    nodes.selectAll('circle')
      .attr('cx', d => scaleX(d.x))
      .attr('cy', d => scaleY(d.y))

    nodes.selectAll('text')
      .attr('x', d => scaleX(d.x) + 15)
      .attr('y', d => scaleY(d.y) + 15)
  }
}

Alex Chirkin
Powered by Hugo