Like I promised in my previous post, I wanted to do a little something on D3.js.
We are going to take one of their example visualizations and visualize a follows graph.
To create our graph, we will take the names of 20 people: create nodes for them, add them to an index, and randomly link them together.
Notice we are using the Neography Batch command to create the whole graph at once.
def create_graph neo = Neography::Rest.new graph_exists = neo.get_node_properties(1) return if graph_exists && graph_exists['name'] names = %w[Max Agam Lester Musannif Adel Andrey Ryan James Bruce Tim Pinaki Mark Peter Anne Helene Corey Ben Rob Pramod Prasanna] commands = names.map{ |n| [:create_node, {"name" => n}]} names.each_index do |x| commands << [:add_node_to_index, "nodes_index", "type", "User", "{#{x}}"] follows = names.size.times.map{|y| y} follows.delete_at(x) follows.sample(1 + rand(5)).each do |f| commands << [:create_relationship, "follows", "{#{x}}", "{#{f}}"] end end batch_result = neo.batch *commands end
We won’t be making the mistake of leaving the create_graph method publicly accessible again, so we’ll create a Rake task for it.
require 'neography/tasks' require './d3.rb' namespace :neo4j do task :create do create_graph end end
We will use Cypher to create a follower matrix, which we will use to populate our D3 script.
def follower_matrix neo = Neography::Rest.new cypher_query = " START a = node:nodes_index(type='User')" cypher_query << " MATCH a-[:follows]->b" cypher_query << " RETURN a.name, collect(b.name)" neo.execute_query(cypher_query)["data"] end
The collect function returns a string with an array inside it, so we have to some string wrangling to turn it into a proper array and then convert everything to JSON.
get '/follows' do follower_matrix.map{|fm| {"name" => fm[0], "follows" => fm[1][1..(fm[1].size - 2)].split(", ")} }.to_json end
Our D3 function is a small variation on the chord flare example in the D3 github repository:
var r1 = 960 / 2, r0 = r1 - 120; var fill = d3.scale.category20c(); var chord = d3.layout.chord() .padding(.04) .sortSubgroups(d3.descending) .sortChords(d3.descending); var arc = d3.svg.arc() .innerRadius(r0) .outerRadius(r0 + 20); var svg = d3.select("body").append("svg") .attr("width", r1 * 2) .attr("height", r1 * 2) .append("g") .attr("transform", "translate(" + r1 + "," + r1 + ")"); function fade(opacity) { return function(g, i) { svg.selectAll("g path.chord") .filter(function(d) { return d.source.index != i && d.target.index != i; }) .transition() .style("opacity", opacity); }; } function draw(follows) { var indexByName = {}, nameByIndex = {}, matrix = [], n = 0; function name(name) { return name } // Compute a unique index for each name. follows.forEach(function(d) { d = name(d.name); if (!(d in indexByName)) { nameByIndex[n] = d; indexByName[d] = n++; } }); // Construct a square matrix counting relationships. follows.forEach(function(d) { var source = indexByName[name(d.name)], row = matrix; if (!row) { row = matrix = []; for (var i = -1; ++i < n;) row[i] = 0; } d.follows.forEach(function(d) { row[indexByName[name(d)]]++; }); }); chord.matrix(matrix); var g = svg.selectAll("g.group") .data(chord.groups) .enter().append("g") .attr("class", "group"); g.append("path") .style("fill", function(d) { return fill(d.index); }) .style("stroke", function(d) { return fill(d.index); }) .attr("d", arc); g.append("text") .each(function(d) { d.angle = (d.startAngle + d.endAngle) / 2; }) .attr("dy", ".35em") .attr("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; }) .attr("transform", function(d) { return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")" + "translate(" + (r0 + 26) + ")" + (d.angle > Math.PI ? "rotate(180)" : ""); }) .text(function(d) { return nameByIndex[d.index]; }); svg.selectAll("path.chord") .data(chord.chords) .enter().append("path") .attr("class", "chord") .style("stroke", function(d) { return d3.rgb(fill(d.source.index)).darker(); }) .style("fill", function(d) { return fill(d.source.index); }) .attr("d", d3.svg.chord().radius(r0)); } d3.json("follows",draw);
All of the code is available on Github.
Finally, we’ll put all this on Heroku, like I’ve shown you before:
git clone git@github.com:maxdemarzi/d3_js_intro.git cd d3_js_intro bundle install heroku create --stack cedar heroku addons:add neo4j git push heroku master heroku run rake neo4j:create
Pretty isn’t it?
Update: The visualization was hard to follow with all those paths, so I added a mouse over function to fade the paths I am not interested in.
Click the image to see the live version and put your mouse over one of the users to see:
That graphic is gorgeous! keep it up man, nice work.
very cool!!!
Great series!
You mention: “We won’t be making the mistake of leaving the create_graph method publicly accessible again, so we’ll create a Rake task for it.”
Is this a coding or security issue? Just curious.
Thanks for all the excellent work you are sharing!
Patrick
A bit of both. For this exercise, we are creating a random static graph and we only need to do this once, so it might as well be a rake task. I had left the create_graph method accessible by going to the site and the url mysite.com/create_graph in a previous example, not expecting people to go there just to try it out.
This example could be more fun if we used the actual Twitter data from people who visited. It would be a good http://neo4j-challenge.herokuapp.com . Anybody up to it?
This is just great!! Keep up the good work.
[…] few weeks ago I showed you how to visualize a graph using the chord flare visualization and how to visualize a network using a force directed graph […]
[…] De Marzi writes: A few weeks ago I showed you how to visualize a graph using the chord flare visualization and how to visualize a network using a force directed graph […]
is there any way to create this project with java ?
Absolutely. The cypher query can be called from any language, and the rest is just boiler plate stuff to create a random graph, put it in a JSON object and pass it on to the Javascript visualization.
Hi Max,
Where can I find an example how to create the graph on the head of the post (grey one) using Neo and D3?
Thanks
Mickey, that’s the default visualization in the Neo4j Web Admin.
Thanks, will take a look. Any idea if it can be extended (like coloring specific routes/adding layers of information)?
Max, where can you see how D3 specifically makes a REST call to the Neo4J RestAPI? I am trying to do a Rest cypher query using only D3 but I can’t find a straight forward example of how to structure the d3.json() call.
http://localhost:7474/webadmin/#/data/visualization/settings/profile/ <– You can add different styles/filters to color different aspects of your graph.
Take a look at http://blog.ohnosequences.com/2011/10/playing-with-microsatellites-simple-sequence-repeats-java-and-neo4j/ for ideas.
Great! Thanks again
[…] are re-using the D3 Chord visualization we saw before and that’s all there is too […]
Hi max,
It is only me or the heroku app is down ? http://morning-sunset-4428.herokuapp.com/index.html
[…] https://maxdemarzi.com/2012/02/02/graph-visualization-and-neo4j-part-three/ […]
It seems like the chord visualization has a limit to the number of nodes/edges. Beyond a point the browser will start to groan?
Yup. Plus it becomes impossible to read. I think the sweet spot is between 20 and 50 nodes.
[…] Graph Visualization and Neo4j – Part Three. Var r1 = 960 / 2, r0 = r1 – 120; var fill = d3.scale.category20c(); var chord = d3.layout.chord() .padding(.04) […]
[…] Neography on Sinatra in Rubyby Max de MarziTweetGraph visualization doesn’t have to be just circles and arrows, as Max demonstrates with this easily grokable project that uses the D3.js library to display a chord diagram of [:follows] relationships in the graph.Read more about it in Max’s blog post. […]
Hi Max,
Thanks for this tutorial. I’m building a web application with node js express js and neo4j. I’m able to do the 4 CRUD functions right now but I want to display the response of my queries like graphs. I’ve tried Neovis and Vis js but It seems like it does’t work with the structure of my app. Did you write any paper about these kinds of app.
Thanks.