block by micahstubbs 8a3779fc211b45ef9744100d1307f0fa

d3 example graph search - user query

Full Screen

This iteration add an interaction that opens the block represented by a node when you click on that node

In this iteration, we load json data from a static file. This json data is produced by the REST API of a neo4j graph database that contains the d3 example README citation graph, in response to a query that we post to it.

This query finds all of the blocks from user enjalot that mention blocks or are mentioned themselves. Then, we show those blocks as well as all 1st degree connections

MATCH(n)-[:LINKS_TO]-(m)
 WHERE n.user =~ '.*enjalot.*'
 RETURN n, m

see also

the parent project for blockbuilder graph search ui prototypes
https://github.com/micahstubbs/bbgs-ui-prototypes

the blockbuilder graph search neo4j graph database source data & config backend project https://github.com/micahstubbs/blockbuilder-graph-search-index

index.html

<!DOCTYPE html>
<meta charset='utf-8'>
<style>
img {
  max-width: 230px;
  max-height: 120px;
}

#canvas-container {
  width: 100%;
  text-align: center;
}

canvas {
  display: inline;
}
</style>
<a target='_blank' style='outline:none;'>
  <div id='canvas-container'>
    <canvas width='960' height='960'>Your browser does not support canvas</canvas>
  </div>
</a>
<script src='https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.js'></script>
<script src="forceInABox.js"></script>
<script src='jsLouvain.js'></script>
<script src='vis.js'></script>

forceInABox.js

/* global d3 */

function forceInABox(alpha) {
  function index(d) {
    return d.index;
  }

  var id = index,
    nodes,
    links, //needed for the force version
    tree,
    size = [100, 100],
    nodeSize = 1, // The expected node size used for computing the cluster node
    forceCharge = -2,
    foci = {},
    // oldStart = force.start,
    linkStrengthIntraCluster = 0.1,
    linkStrengthInterCluster = 0.01,
    // oldGravity = force.gravity(),
    templateNodes = [],
    offset = [0, 0],
    templateForce,
    templateNodesSel,
    groupBy = function(d) {
      return d.cluster;
    },
    template = 'treemap',
    enableGrouping = true,
    strength = 0.1;
  // showingTemplate = false;

  function force(alpha) {
    if (!enableGrouping) {
      return force;
    }
    if (template === 'force') {
      //Do the tick of the template force and get the new focis
      templateForce.tick();
      getFocisFromTemplate();
    }

    for (var i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
      node = nodes[i];
      node.vx += (foci[groupBy(node)].x - node.x) * k;
      node.vy += (foci[groupBy(node)].y - node.y) * k;
    }
  }

  function initialize() {
    if (!nodes) return;

    // var i,
    //     n = nodes.length,
    //     m = links.length,
    //     nodeById = map(nodes, id),
    //     link;

    if (template === 'treemap') {
      initializeWithTreemap();
    } else {
      initializeWithForce();
    }
  }

  force.initialize = function(_) {
    nodes = _;
    initialize();
  };

  function getLinkKey(l) {
    var sourceID = groupBy(l.source), targetID = groupBy(l.target);

    return sourceID <= targetID
      ? sourceID + '~' + targetID
      : targetID + '~' + sourceID;
  }

  function computeClustersNodeCounts(nodes) {
    var clustersCounts = d3.map();

    nodes.forEach(function(d) {
      if (!clustersCounts.has(groupBy(d))) {
        clustersCounts.set(groupBy(d), 0);
      }
    });

    nodes.forEach(function(d) {
      // if (!d.show) { return; }
      clustersCounts.set(groupBy(d), clustersCounts.get(groupBy(d)) + 1);
    });

    return clustersCounts;
  }

  //Returns
  function computeClustersLinkCounts(links) {
    var dClusterLinks = d3.map(), clusterLinks = [];
    links.forEach(function(l) {
      var key = getLinkKey(l), count;
      if (dClusterLinks.has(key)) {
        count = dClusterLinks.get(key);
      } else {
        count = 0;
      }
      count += 1;
      dClusterLinks.set(key, count);
    });

    dClusterLinks.entries().forEach(function(d) {
      var source, target;
      source = d.key.split('~')[0];
      target = d.key.split('~')[1];
      clusterLinks.push({
        source: source,
        target: target,
        count: d.value
      });
    });
    return clusterLinks;
  }

  //Returns the metagraph of the clusters
  function getGroupsGraph() {
    var gnodes = [],
      glinks = [],
      // edges = [],
      dNodes = d3.map(),
      // totalSize = 0,
      clustersList,
      c,
      i,
      size,
      clustersCounts,
      clustersLinks;

    clustersCounts = computeClustersNodeCounts(nodes);
    clustersLinks = computeClustersLinkCounts(links);

    //map.keys() is really slow, it's crucial to have it outside the loop
    clustersList = clustersCounts.keys();
    for (i = 0; i < clustersList.length; i += 1) {
      c = clustersList[i];
      size = clustersCounts.get(c);
      gnodes.push({ id: c, size: size });
      dNodes.set(c, i);
      // totalSize += size;
    }

    clustersLinks.forEach(function(l) {
      glinks.push({
        source: dNodes.get(l.source),
        target: dNodes.get(l.target),
        count: l.count
      });
    });

    return { nodes: gnodes, links: glinks };
  }

  function getGroupsTree() {
    var children = [], totalSize = 0, clustersList, c, i, size, clustersCounts;

    clustersCounts = computeClustersNodeCounts(force.nodes());

    //map.keys() is really slow, it's crucial to have it outside the loop
    clustersList = clustersCounts.keys();
    for (i = 0; i < clustersList.length; i += 1) {
      c = clustersList[i];
      size = clustersCounts.get(c);
      children.push({ id: c, size: size });
      totalSize += size;
    }
    // return {id: "clustersTree", size: totalSize, children : children};
    return { id: 'clustersTree', children: children };
  }

  function getFocisFromTemplate() {
    //compute foci
    foci.none = { x: 0, y: 0 };
    templateNodes.forEach(function(d) {
      if (template === 'treemap') {
        foci[d.data.id] = {
          x: d.x0 + (d.x1 - d.x0) / 2 - offset[0],
          y: d.y0 + (d.y1 - d.y0) / 2 - offset[1]
        };
      } else {
        foci[d.id] = { x: d.x - offset[0], y: d.y - offset[1] };
      }
    });
  }

  function initializeWithTreemap() {
    var treemap = d3.treemap().size(force.size());

    tree = d3
      .hierarchy(getGroupsTree())
      // .sort(function (p, q) { return d3.ascending(p.size, q.size); })
      // .count()
      .sum(function(d) {
        return d.size;
      })
      .sort(function(a, b) {
        return b.height - a.height || b.value - a.value;
      });

    templateNodes = treemap(tree).leaves();

    getFocisFromTemplate();
  }

  function checkLinksAsObjects() {
    // Check if links come in the format of indexes instead of objects
    var linkCount = 0;
    if (nodes.length === 0) return;
    // console.log('nodes from forceInABox', nodes);

    links.forEach(function(link) {
      var source, target;
      if (!nodes) return;
      source = link.source;
      target = link.target;
      if (typeof link.source !== 'object') source = nodes[link.source];
      if (typeof link.target !== 'object') target = nodes[link.target];
      if (source === undefined || target === undefined) {
        console.log('link from forceInABox', link);
        throw Error(
          "Error setting links, couldn't find nodes for a link (see it on the console)"
        );
      }
      link.source = source;
      link.target = target;
      link.index = linkCount++;
    });
  }

  function initializeWithForce() {
    var net;

    if (nodes && nodes.length > 0) {
      if (groupBy(nodes[0]) === undefined) {
        throw Error(
          "Couldn't find the grouping attribute for the nodes. Make sure to set it up with forceInABox.groupBy('attr') before calling .links()"
        );
      }
    }

    checkLinksAsObjects();

    net = getGroupsGraph();
    templateForce = d3
      .forceSimulation(net.nodes)
      .force('x', d3.forceX(size[0] / 2).strength(0.5))
      .force('y', d3.forceY(size[1] / 2).strength(0.5))
      .force(
        'collide',
        d3.forceCollide(function(d) {
          return d.size * nodeSize;
        })
      )
      .force(
        'charge',
        d3.forceManyBody().strength(function(d) {
          return forceCharge * d.size;
        })
      )
      .force('links', d3.forceLink(!net.nodes ? net.links : []));

    templateNodes = templateForce.nodes();

    getFocisFromTemplate();
  }

  function drawTreemap(container) {
    container.selectAll('.cell').remove();
    container
      .selectAll('cell')
      .data(templateNodes)
      .enter()
      .append('svg:rect')
      .attr('class', 'cell')
      .attr('x', function(d) {
        return d.x0;
      })
      .attr('y', function(d) {
        return d.y0;
      })
      .attr('width', function(d) {
        return d.x1 - d.x0;
      })
      .attr('height', function(d) {
        return d.y1 - d.y0;
      });
  }

  function drawGraph(container) {
    container.selectAll('.cell').remove();
    templateNodesSel = container.selectAll('cell').data(templateNodes);
    templateNodesSel
      .enter()
      .append('svg:circle')
      .attr('class', 'cell')
      .attr('cx', function(d) {
        return d.x;
      })
      .attr('cy', function(d) {
        return d.y;
      })
      .attr('r', function(d) {
        return d.size * nodeSize;
      });
  }

  force.drawTemplate = function(container) {
    // showingTemplate = true;
    if (template === 'treemap') {
      drawTreemap(container);
    } else {
      drawGraph(container);
    }
    return force;
  };

  //Backwards compatibility
  force.drawTreemap = force.drawTemplate;

  force.deleteTemplate = function(container) {
    // showingTemplate = false;
    container.selectAll('.cell').remove();

    return force;
  };

  force.template = function(x) {
    if (!arguments.length) return template;
    template = x;
    initialize();
    return force;
  };

  force.groupBy = function(x) {
    if (!arguments.length) return groupBy;
    if (typeof x === 'string') {
      groupBy = function(d) {
        return d[x];
      };
      return force;
    }
    groupBy = x;
    return force;
  };

  force.enableGrouping = function(x) {
    if (!arguments.length) return enableGrouping;
    enableGrouping = x;
    // update();
    return force;
  };

  force.strength = function(x) {
    if (!arguments.length) return strength;
    strength = x;
    return force;
  };

  force.getLinkStrength = function(e) {
    if (enableGrouping) {
      if (groupBy(e.source) === groupBy(e.target)) {
        if (typeof linkStrengthIntraCluster === 'function') {
          return linkStrengthIntraCluster(e);
        } else {
          return linkStrengthIntraCluster;
        }
      } else {
        if (typeof linkStrengthInterCluster === 'function') {
          return linkStrengthInterCluster(e);
        } else {
          return linkStrengthInterCluster;
        }
      }
    } else {
      // Not grouping return the intracluster
      if (typeof linkStrengthIntraCluster === 'function') {
        return linkStrengthIntraCluster(e);
      } else {
        return linkStrengthIntraCluster;
      }
    }
  };

  force.id = function(_) {
    return arguments.length ? ((id = _), force) : id;
  };

  force.size = function(_) {
    return arguments.length ? ((size = _), force) : size;
  };

  force.linkStrengthInterCluster = function(_) {
    return arguments.length
      ? ((linkStrengthInterCluster = _), force)
      : linkStrengthInterCluster;
  };

  force.linkStrengthIntraCluster = function(_) {
    return arguments.length
      ? ((linkStrengthIntraCluster = _), force)
      : linkStrengthIntraCluster;
  };

  force.nodes = function(_) {
    return arguments.length ? ((nodes = _), force) : nodes;
  };

  force.links = function(_) {
    if (!arguments.length) return links;
    if (_ === null) links = [];
    else links = _;
    return force;
  };

  force.nodeSize = function(_) {
    return arguments.length ? ((nodeSize = _), force) : nodeSize;
  };

  force.forceCharge = function(_) {
    return arguments.length ? ((forceCharge = _), force) : forceCharge;
  };

  force.offset = function(_) {
    return arguments.length ? ((offset = _), force) : offset;
  };

  return force;
}

jsLouvain.js

/*
Author: Corneliu S. (github.com/upphiminn)

This is a javascript implementation of the Louvain
community detection algorithm (http://arxiv.org/abs/0803.0476)
Based on https://bitbucket.org/taynaud/python-louvain/overview

*/
(function() {
  jLouvain = function() {
    //Constants
    var __PASS_MAX = -1;
    var __MIN = 0.0000001;

    //Local vars
    var original_graph_nodes;
    var original_graph_edges;
    var original_graph = {};
    var partition_init;

    //Helpers
    function make_set(array) {
      var set = {};
      array.forEach(function(d, i) {
        set[d] = true;
      });
      return Object.keys(set);
    }

    function obj_values(obj) {
      var vals = [];
      for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
          vals.push(obj[key]);
        }
      }
      return vals;
    }

    function get_degree_for_node(graph, node) {
      var neighbours = graph._assoc_mat[node]
        ? Object.keys(graph._assoc_mat[node])
        : [];
      var weight = 0;
      neighbours.forEach(function(neighbour, i) {
        var value = graph._assoc_mat[node][neighbour] || 1;
        if (node == neighbour) value *= 2;
        weight += value;
      });
      return weight;
    }

    function get_neighbours_of_node(graph, node) {
      if (typeof graph._assoc_mat[node] == 'undefined') return [];

      var neighbours = Object.keys(graph._assoc_mat[node]);
      return neighbours;
    }

    function get_edge_weight(graph, node1, node2) {
      return graph._assoc_mat[node1]
        ? graph._assoc_mat[node1][node2]
        : undefined;
    }

    function get_graph_size(graph) {
      var size = 0;
      graph.edges.forEach(function(edge) {
        size += edge.weight;
      });
      return size;
    }

    function add_edge_to_graph(graph, edge) {
      update_assoc_mat(graph, edge);

      var edge_index = graph.edges
        .map(function(d) {
          return d.source + '_' + d.target;
        })
        .indexOf(edge.source + '_' + edge.target);

      if (edge_index != -1) graph.edges[edge_index].weight = edge.weight;
      else graph.edges.push(edge);
    }

    function make_assoc_mat(edge_list) {
      var mat = {};
      edge_list.forEach(function(edge, i) {
        mat[edge.source] = mat[edge.source] || {};
        mat[edge.source][edge.target] = edge.weight;
        mat[edge.target] = mat[edge.target] || {};
        mat[edge.target][edge.source] = edge.weight;
      });

      return mat;
    }

    function update_assoc_mat(graph, edge) {
      graph._assoc_mat[edge.source] = graph._assoc_mat[edge.source] || {};
      graph._assoc_mat[edge.source][edge.target] = edge.weight;
      graph._assoc_mat[edge.target] = graph._assoc_mat[edge.target] || {};
      graph._assoc_mat[edge.target][edge.source] = edge.weight;
    }

    function clone(obj) {
      if (obj == null || typeof obj != 'object') return obj;

      var temp = obj.constructor();

      for (var key in obj)
        temp[key] = clone(obj[key]);
      return temp;
    }

    //Core-Algorithm Related
    function init_status(graph, status, part) {
      status['nodes_to_com'] = {};
      status['total_weight'] = 0;
      status['internals'] = {};
      status['degrees'] = {};
      status['gdegrees'] = {};
      status['loops'] = {};
      status['total_weight'] = get_graph_size(graph);

      if (typeof part == 'undefined') {
        graph.nodes.forEach(function(node, i) {
          status.nodes_to_com[node] = i;
          var deg = get_degree_for_node(graph, node);
          if (deg < 0) throw 'Bad graph type, use positive weights!';
          status.degrees[i] = deg;
          status.gdegrees[node] = deg;
          status.loops[node] = get_edge_weight(graph, node, node) || 0;
          status.internals[i] = status.loops[node];
        });
      } else {
        graph.nodes.forEach(function(node, i) {
          var com = part[node];
          status.nodes_to_com[node] = com;
          var deg = get_degree_for_node(graph, node);
          status.degrees[com] = (status.degrees[com] || 0) + deg;
          status.gdegrees[node] = deg;
          var inc = 0.0;

          var neighbours = get_neighbours_of_node(graph, node);
          neighbours.forEach(function(neighbour, i) {
            var weight = graph._assoc_mat[node][neighbour];
            if (weight <= 0) {
              throw 'Bad graph type, use positive weights';
            }

            if (part[neighbour] == com) {
              if (neighbour == node) {
                inc += weight;
              } else {
                inc += weight / 2.0;
              }
            }
          });
          status.internals[com] = (status.internals[com] || 0) + inc;
        });
      }
    }

    function __modularity(status) {
      var links = status.total_weight;
      var result = 0.0;
      var communities = make_set(obj_values(status.nodes_to_com));

      communities.forEach(function(com, i) {
        var in_degree = status.internals[com] || 0;
        var degree = status.degrees[com] || 0;
        if (links > 0) {
          result =
            result + in_degree / links - Math.pow(degree / (2.0 * links), 2);
        }
      });
      return result;
    }

    function __neighcom(node, graph, status) {
      // compute the communities in the neighb. of the node, with the graph given by
      // node_to_com

      var weights = {};
      var neighboorhood = get_neighbours_of_node(graph, node); //make iterable;

      neighboorhood.forEach(function(neighbour, i) {
        if (neighbour != node) {
          var weight = graph._assoc_mat[node][neighbour] || 1;
          var neighbourcom = status.nodes_to_com[neighbour];
          weights[neighbourcom] = (weights[neighbourcom] || 0) + weight;
        }
      });

      return weights;
    }

    function __insert(node, com, weight, status) {
      //insert node into com and modify status
      status.nodes_to_com[node] = +com;
      status.degrees[com] =
        (status.degrees[com] || 0) + (status.gdegrees[node] || 0);
      status.internals[com] =
        (status.internals[com] || 0) + weight + (status.loops[node] || 0);
    }

    function __remove(node, com, weight, status) {
      //remove node from com and modify status
      status.degrees[com] =
        (status.degrees[com] || 0) - (status.gdegrees[node] || 0);
      status.internals[com] =
        (status.internals[com] || 0) - weight - (status.loops[node] || 0);
      status.nodes_to_com[node] = -1;
    }

    function __renumber(dict) {
      var count = 0;
      var ret = clone(dict); //deep copy :)
      var new_values = {};
      var dict_keys = Object.keys(dict);
      dict_keys.forEach(function(key) {
        var value = dict[key];
        var new_value = typeof new_values[value] == 'undefined'
          ? -1
          : new_values[value];
        if (new_value == -1) {
          new_values[value] = count;
          new_value = count;
          count = count + 1;
        }
        ret[key] = new_value;
      });
      return ret;
    }

    function __one_level(graph, status) {
      //Compute one level of the Communities Dendogram.
      var modif = true,
        nb_pass_done = 0,
        cur_mod = __modularity(status),
        new_mod = cur_mod;

      while (modif && nb_pass_done != __PASS_MAX) {
        cur_mod = new_mod;
        modif = false;
        nb_pass_done += 1;

        graph.nodes.forEach(function(node, i) {
          var com_node = status.nodes_to_com[node];
          var degc_totw =
            (status.gdegrees[node] || 0) / (status.total_weight * 2.0);
          var neigh_communities = __neighcom(node, graph, status);
          __remove(node, com_node, neigh_communities[com_node] || 0.0, status);
          var best_com = com_node;
          var best_increase = 0;
          var neigh_communities_entries = Object.keys(neigh_communities); //make iterable;

          neigh_communities_entries.forEach(function(com, i) {
            var incr =
              neigh_communities[com] - (status.degrees[com] || 0.0) * degc_totw;
            if (incr > best_increase) {
              best_increase = incr;
              best_com = com;
            }
          });

          __insert(node, best_com, neigh_communities[best_com] || 0, status);

          if (best_com != com_node) modif = true;
        });
        new_mod = __modularity(status);
        if (new_mod - cur_mod < __MIN) break;
      }
    }

    function induced_graph(partition, graph) {
      var ret = { nodes: [], edges: [], _assoc_mat: {} };
      var w_prec, weight;
      //add nodes from partition values
      var partition_values = obj_values(partition);
      ret.nodes = ret.nodes.concat(make_set(partition_values)); //make set
      graph.edges.forEach(function(edge, i) {
        weight = edge.weight || 1;
        var com1 = partition[edge.source];
        var com2 = partition[edge.target];
        w_prec = get_edge_weight(ret, com1, com2) || 0;
        var new_weight = w_prec + weight;
        add_edge_to_graph(ret, {
          source: com1,
          target: com2,
          weight: new_weight
        });
      });
      return ret;
    }

    function partition_at_level(dendogram, level) {
      var partition = clone(dendogram[0]);
      for (var i = 1; i < level + 1; i++)
        Object.keys(partition).forEach(function(key, j) {
          var node = key;
          var com = partition[key];
          partition[node] = dendogram[i][com];
        });
      return partition;
    }

    function generate_dendogram(graph, part_init) {
      if (graph.edges.length == 0) {
        var part = {};
        graph.nodes.forEach(function(node, i) {
          part[node] = node;
        });
        return part;
      }
      var status = {};

      init_status(original_graph, status, part_init);
      var mod = __modularity(status);
      var status_list = [];
      __one_level(original_graph, status);
      var new_mod = __modularity(status);
      var partition = __renumber(status.nodes_to_com);
      status_list.push(partition);
      mod = new_mod;
      var current_graph = induced_graph(partition, original_graph);
      init_status(current_graph, status);

      while (true) {
        __one_level(current_graph, status);
        new_mod = __modularity(status);
        if (new_mod - mod < __MIN) break;

        partition = __renumber(status.nodes_to_com);
        status_list.push(partition);

        mod = new_mod;
        current_graph = induced_graph(partition, current_graph);
        init_status(current_graph, status);
      }

      return status_list;
    }

    var core = function() {
      var status = {};
      var dendogram = generate_dendogram(original_graph, partition_init);
      return partition_at_level(dendogram, dendogram.length - 1);
    };

    core.nodes = function(nds) {
      if (arguments.length > 0) {
        original_graph_nodes = nds;
      }
      return core;
    };

    core.edges = function(edgs) {
      if (typeof original_graph_nodes == 'undefined')
        throw 'Please provide the graph nodes first!';

      if (arguments.length > 0) {
        original_graph_edges = edgs;
        var assoc_mat = make_assoc_mat(edgs);
        original_graph = {
          nodes: original_graph_nodes,
          edges: original_graph_edges,
          _assoc_mat: assoc_mat
        };
      }
      return core;
    };

    core.partition_init = function(prttn) {
      if (arguments.length > 0) {
        partition_init = prttn;
      }
      return core;
    };

    return core;
  };
})();

vis.js

const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
const searchRadius = 30;

const color = d3.scaleOrdinal().range(d3.schemeCategory20);

// Create the simulation with a small forceX and Y towards the center
var simulation = d3
  .forceSimulation()
  .force('charge', d3.forceManyBody())
  .force('x', d3.forceX(0).strength(0.003))
  .force('y', d3.forceY(0).strength(0.003));

//
// comment out for now, until we have a public neo4j instance running
// https://think-lab.github.io/d/216/#1
//

//
// make the request to neo4j for the data
//
// function fetchGraphSearchResults(queryString) {
//   const url = 'http://localhost:7474/db/data/transaction/commit';
//   const requestData = JSON.stringify({
//     statements: [
//       {
//         statement: queryString
//       }
//     ]
//   });
//   const myHeaders = new Headers();
//   myHeaders.append('Content-Type', 'application/json');
//   myHeaders.append('Authorization', 'Basic bmVvNGo6YWRtaW4=');
//   myHeaders.append('Accept', 'application/json; charset=UTF-8');
//   const myInit = {
//     method: 'POST',
//     body: requestData,
//     headers: myHeaders
//   };
//   const myRequest = new Request(url, myInit);
//   fetch(myRequest)
//     .then(response => response.json())
//     .then(data => parseResponse(data))
//     .catch(e => {
//       console.log(e);
//     });
// }

//
// run a defult query so the user has
// something nice to look at on load
//
// const mapQueryString =
//   "MATCH(n)-[:LINKS_TO]-(m) WHERE n.description =~  '.*map.*'RETURN n, m";
// const enjalotQueryString =
//   "MATCH(n)-[:LINKS_TO]-(m) WHERE n.user =~ '.*enjalot.*'RETURN n, m";
// fetchGraphSearchResults(enjalotQueryString);

//
// load static neo4j API response data from a file
//
d3.json('neo4j-api-response.json', (error, response) => {
  if (error) {
    console.error(error);
  }
  parseResponse(response);
});

//
// cache images
//
const imageCache = {};

//
// parse the response from neo4j
//
function parseResponse(responseData) {
  const graph = {
    nodes: [],
    links: []
  };
  const nodeHash = {};

  console.log('responseData from parseResponse', responseData);
  const graphData = responseData.results[0].data;
  graphData.forEach(inputLink => {
    const source = inputLink.row[0].gistId;
    const target = inputLink.row[1].gistId;
    if (typeof source !== 'undefined' && typeof target !== 'undefined') {
      // collect the nodes in a set
      // which builds up a list of unique nodes
      inputLink.row.forEach(inputNode => {
        nodeHash[inputNode.gistId] = {
          id: inputNode.gistId,
          createdAt: inputNode.createdAt,
          description: inputNode.description,
          updatedAt: inputNode.updatedAt,
          user: inputNode.user
        };
      });
      // assume that the inputLink rows
      // are in [source, target] format
      // TODO: check the neo4j REST API docs
      // to verify this
      graph.links.push({
        source,
        target,
        weight: 1 // for jsLouvain community detection
      });
    }
  });

  // add the unique nodes that we've collected
  // onto our graph object
  Object.keys(nodeHash).forEach(key => {
    graph.nodes.push(nodeHash[key]);
  });

  // call the drawGraph function
  // to plot the graph
  drawGraph(graph);
}

//
// visualize the graph
//
function drawGraph(graph) {
  console.log('graph from drawGraph', graph);
  cacheImages(graph, imageCache);

  // clear the canvas
  context.clearRect(0, 0, canvas.width, canvas.height);

  //
  // detect communities with jsLouvain
  //
  const nodeData = graph.nodes.map(function(d) {
    return d.id;
  });
  const linkData = graph.links.map(function(d) {
    return { source: d.source, target: d.target, weight: d.weight };
  });

  const community = jLouvain().nodes(nodeData).edges(linkData);
  const result = community();

  const nodeIndexHash = {};
  graph.nodes.forEach(function(node, i) {
    node.group = result[node.id];
    nodeIndexHash[node.id] = i;
  });

  //
  //
  //
  const users = d3
    .nest()
    .key(d => d.user)
    .entries(graph.nodes)
    .sort((a, b) => b.values.length - a.values.length);

  color.domain(users.map(d => d.key));

  //
  // process links data to use simple node array index ids
  // for source and target values
  // to satisfy the assumption of the forceInABox layout
  //
  graph.links.forEach(link => {
    // record the gistId
    link.sourceGistId = link.source;
    link.targetGistId = link.target;

    // now use the node index
    link.source = nodeIndexHash[link.source];
    link.target = nodeIndexHash[link.target];
  });

  //
  // Instantiate the forceInABox force
  //
  const groupingForce = forceInABox()
    .strength(0.001) // Strength to foci
    .template('force') // Either treemap or force
    .groupBy('group') // Node attribute to group
    .links(graph.links) // The graph links. Must be called after setting the grouping attribute
    .size([width, height]); // Size of the chart

  // Add your forceInABox to the simulation
  simulation
    .nodes(graph.nodes)
    .force('group', groupingForce)
    .force(
      'link',
      d3
        .forceLink(graph.links)
        .distance(50)
        .strength(groupingForce.getLinkStrength) // default link force will try to join nodes in the same group stronger than if they are in different groups
    )
    .on('tick', ticked);

  // simulation.force('link').links(graph.links);

  d3
    .select(canvas)
    .on('mousemove', mousemoved)
    .on('click', clicked)
    .call(
      d3
        .drag()
        .container(canvas)
        .subject(dragsubject)
        .on('start', dragstarted)
        .on('drag', dragged)
        .on('end', dragended)
    );

  function ticked() {
    context.clearRect(0, 0, width, height);
    context.save();
    context.translate(width / 2, height / 2);

    context.beginPath();
    graph.links.forEach(drawLink);
    context.strokeStyle = '#aaa';
    context.stroke();

    users.forEach(user => {
      context.beginPath();
      user.values.forEach(drawNode);
      context.fillStyle = color(user.key);
      context.fill();
    });

    context.restore();
  }

  function dragsubject() {
    return simulation.find(
      d3.event.x - width / 2,
      d3.event.y - height / 2,
      searchRadius
    );
  }

  function mousemoved() {
    //
    // disable mouse move links for now
    //
    const a = this.parentNode;
    const m = d3.mouse(this);
    const d = simulation.find(
      m[0] - width / 2,
      m[1] - height / 2,
      searchRadius
    );
    if (!d) return a.removeAttribute('href');
    a.removeAttribute('title');
    a.setAttribute(
      'href',
      `http://bl.ocks.org/${d.user ? `${d.user}/` : ''}${d.id}`
    );
    a.setAttribute(
      'title',
      `${d.id}${d.user ? ` by ${d.user}` : ''}${d.description
        ? `\n${d.description}`
        : ''}`
    );
  }

  function clicked() {
    const m = d3.mouse(this);
    const d = simulation.find(
      m[0] - width / 2,
      m[1] - height / 2,
      searchRadius
    );
    const blockUrl = `http://bl.ocks.org/${d.user ? `${d.user}/` : ''}${d.id}`;
    window.open(blockUrl);
  }
}

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

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

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

// a small function to ensure that
// points stay inside the canvas
function boundScalar(p) {
  const halfEdge = 448;
  const minP = Math.min(p, halfEdge);
  return Math.max(-halfEdge, minP);
}

function drawLink(d) {
  context.moveTo(boundScalar(d.source.x), boundScalar(d.source.y));
  context.lineTo(boundScalar(d.target.x), boundScalar(d.target.y));
}

function drawNode(d) {
  // old solid color nodes
  // context.moveTo(d.x + 3, d.y);
  // context.arc(d.x, d.y, 3, 0, 2 * Math.PI);

  const image = imageCache[d.id];
  const iconWidth = 92;
  const iconHeight = 48;
  const radius = 22;

  // draw border to check intution
  context.strokeStyle = 'darkgray';
  context.strokeRect(-width / 2, -height / 2, width - 2, height - 2);

  // const minX = Math.min(d.x, width - radius);
  // const nX = Math.max(-width / 2 + radius, minX);

  // const minY = Math.min(d.y, height - 2 * radius);
  // const nY = Math.max(-height / 2 - radius, minY);

  // const minX = Math.min(d.x, 448);
  // const nX = Math.max(-448, minX);

  // const minY = Math.min(d.y, 448);
  // const nY = Math.max(-448, minY);

  const nX = boundScalar(d.x);
  const nY = boundScalar(d.y);

  // if (d.x !== nX || d.y !== nY) {
  //   console.log('d.x', d.x);
  //   console.log('d.y', d.y);
  //   console.log('nX', nX);
  //   console.log('nY', nY);
  //   console.log('------------');
  // }

  // draw images with a circular clip mask
  // so that rectangular thumbnail images become
  // round node icons
  if (typeof image !== 'undefined' && image.height > 0) {
    context.save();
    context.beginPath();
    context.arc(nX, nY, radius, 0, Math.PI * 2, true);
    context.closePath();
    context.clip();

    context.drawImage(
      image,
      0,
      0,
      230,
      120,
      nX - iconWidth / 2,
      nY - iconHeight / 2,
      iconWidth,
      iconHeight
    );

    context.beginPath();
    context.arc(0, 0, 2, 0, Math.PI * 2, true);
    context.clip();
    context.closePath();
    context.restore();
  } else {
    // gray from the blockbuilder search results page
    context.fillStyle = '#EEEEEE';
    context.beginPath();
    context.arc(nX, nY, radius, 0, Math.PI * 2, true);
    context.closePath();
    context.fill();

    // teal from blockbuilder search results page
    context.fillStyle = '#66B5B4';
    context.font = '20px Georgia';
    context.fillText('?', nX - 5, nY + 8);
  }
}

function cacheImages(graph, imageCache) {
  graph.nodes.forEach(d => {
    const image = new Image();

    image.src = `https://bl.ocks.org/${d.user
      ? `${d.user}/`
      : ''}raw/${d.id}/thumbnail.png`;
    // image.onload = function() {
    //   imageCache[d.id] = image;
    // };
    imageCache[d.id] = image;
  });
}