Newer
Older
indexation / public / js / force-directed-graph.js
"use strict";

var defaultSubtitle = d3.select(".subtitle").text();

function initGraph(url, corpus) {
  // VAR
  var loc = window.location.pathname,
    dir = loc.substring(0, loc.lastIndexOf('/')),
    corpus = JSON.parse(corpus);

  d3.json(url, function(error, graph) {
    if (error) throw error;

    var canvas = document.querySelector("canvas"),
      d3Canvas = d3.select("canvas"),
      color = d3.scaleOrdinal(d3.schemeCategory20),
      width = d3Canvas.attr("width"),
      height = d3Canvas.attr("height"),
      radius = 4,
      context = canvas.getContext("2d");

    var simulation = d3.forceSimulation()
      .force("link", d3.forceLink().id(function(d) {
        return d.id;
      }))
      .force("charge", d3.forceManyBody())
      .force("center", d3.forceCenter(width / 2, height / 2));

    // FILTER & COMMUNITY
    var selectedData = getSelectedData(setGroup(graph), 4),
      selectedNode = null,
      nodes = selectedData.nodes,
      links = selectedData.links;

    // Set radius
    nodes.forEach(function(e) {
      e.radius = radius;
      return e;
    });

    // FILL CANVAS
    simulation
      .nodes(nodes)
      .on("tick", draw);

    simulation.force("link")
      .links(links);

    // ZOOM
    var transform = d3.zoomIdentity;

    d3Canvas
      .call(d3.drag().subject(dragsubject)
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended))
      .call(d3.zoom().scaleExtent([-100, 100]).on("zoom", zoomed))
      .call(draw);

    d3Canvas
      .on("click", click);

    function draw() {
      context.save();
      context.clearRect(0, 0, width, height);
      context.translate(transform.x, transform.y);
      context.scale(transform.k, transform.k);

      // draw links
      context.strokeStyle = "#546e7a";
      context.beginPath();
      graph.links.forEach(function(d) {
        context.moveTo(Math.floor(d.source.x), Math.floor(d.source.y));
        context.lineTo(Math.floor(d.target.x), Math.floor(d.target.y));
        context.lineWidth = 1;
      });
      context.stroke();

      // draw nodes
      graph.nodes.forEach(function(d) {
        context.beginPath(); //for each node do begin path as context fill style and stroke are different.
        context.strokeStyle = "#fff";
        context.fillStyle = color(d.group);
        if (d.clicked) context.rect(Math.floor(d.x - d.radius), Math.floor(d.y - d.radius), d.radius * 2, d.radius * 2);
        else context.rect(Math.floor(d.x - (d.radius / 2)), Math.floor(d.y - (d.radius / 2)), d.radius, d.radius);
        // context.arc(d.x, d.y, d.radius, 0, 2 * Math.PI);
        context.fill();
        context.stroke();
      });

      context.restore();
    }

    function click() {
      var position = d3.mouse(this),
        node = detectCollision({
          'x': transform.invertX(position[0]),
          'y': transform.invertY(position[1])
        }, nodes);
      if (selectedNode && selectedNode.id !== node.id) selectedNode.clicked = false;
      if (node) node.clicked = !node.clicked; // change clicked status
      selectedNode = node; // set current node in selectedNode
      showNodeInfos(selectedNode);
    }

    function dragsubject() {
      var node = detectCollision({
        'x': transform.invertX(d3.event.x),
        'y': transform.invertY(d3.event.y)
      }, nodes);
      if (node) {
        node.x = transform.applyX(node.x);
        node.y = transform.applyY(node.y);
      }
      return node;
    }

    function dragstarted() {
      if (!d3.event.active) simulation.alphaTarget(0.3).restart();
      d3.event.subject.fx = transform.invertX(d3.event.subject.x);
      d3.event.subject.fy = transform.invertY(d3.event.subject.y);
    }

    function dragged() {
      d3.event.subject.fx = transform.invertX(d3.event.x);
      d3.event.subject.fy = transform.invertY(d3.event.y);
    }

    function dragended() {
      if (!d3.event.active) simulation.alphaTarget(0);
      d3.event.subject.fx = null;
      d3.event.subject.fy = null;
    }

    function zoomed() {
      transform = d3.event.transform;
      draw();
    }
  });
}

function showNodeInfos(node) {
  if (node && node.clicked) {
    d3.select(".subtitle").text(node.data.id);
  } else {
    d3.select(".subtitle").text(defaultSubtitle);
  }
}

function detectCollision(source, targets) {
  var result = null,
    target,
    i,
    dx,
    dy;

  for (i = targets.length - 1; i >= 0; --i) {
    target = targets[i];
    dx = Math.floor(source.x - target.x);
    dy = Math.floor(source.y - target.y);
    if ((dx * dx + dy * dy) < (target.radius * target.radius)) {
      result = target;
    }
  }
  return result;
}

// Set a group for each nodes in graph
function setGroup(graph) {
  var edges = [],
    nodes = [];
  for (var i = 0; i < graph.nodes.length; i++) {
    nodes.push(i);
  }
  for (var i = 0; i < graph.links.length; i++) {
    var edge = graph.links[i];
    edge.weight = edge.value;
    edges.push(edge);
  }

  // Create the "community"
  var community = jLouvain().nodes(nodes).edges(edges),
    res = community();

  // Affect community for each node
  for (var key in res) {
    graph.nodes[nodes[key]].group = res[key];
  }

  return graph;
}

function getSelectedData(graph, lim) {
  var deleted_links = {},
    selectedLinks = graph.links.filter(function(d) {
      var res = (d.value >= lim);
      if (!res) {
        deleted_links[d.source] = deleted_links[d.source] + 1 || 1;
        deleted_links[d.target] = deleted_links[d.target] + 1 || 1;
      }
      return res;
    }),
    selectedNodes = graph.nodes.filter(function(d) {
      d.value = d.value - (deleted_links[d.id] || 0);
      var res = (d.value > 0);
      return res;
    });

  return {
    'links': selectedLinks,
    'nodes': selectedNodes
  };
}