Skip to content

Instantly share code, notes, and snippets.

@markjlorenz
Created September 18, 2012 17:06
Show Gist options
  • Save markjlorenz/3744338 to your computer and use it in GitHub Desktop.
Save markjlorenz/3744338 to your computer and use it in GitHub Desktop.
Rails and Coffee to render an object graph for any rails model
#As coded this is suiteable for adding to ApplicationController, or an included controller module
#It would also be cool to refactor this code to be added to fat models instead
def object_graph
@prune_nodes = params[:prune_nodes] #When the crawler hits a node of this class, it will stop
model_name = controller_name.classify
root_object = model_name.classify.constantize.find(params[:id])
edge_list = {} #This is implemented as an object instead of an array for lookup performance reasons
nodes = {}
object_ident = ->(object){"#{object.id}-#{object.class}"} #helper function
fill_object_data = ->(object, ident) do #add some data to the JSON object that will be used by the renderer
edge_list[ident] = [] if !edge_list[ident] #initialize if needed
unless nodes[ident]
nodes[ident] = {
id:object.id,
type:object.class.to_s,
href:(url_for(object) rescue nil), #if there are no routes for this object leave it blank
name:ident,
description: (object.web_description rescue "") #if you add #web_description to the models it will be used here
}
end
end
traverse = ->(object) do #DFS over the model object graph by reflecting on associations
object.class.reflect_on_all_associations.each do |assoc|
this_object_ident = object_ident.call object
fill_object_data.call object, this_object_ident
associated_objects = Array.wrap object.send(assoc.name) #Call the association on the object i.e. `foo.bars`
associated_objects.each do |assoc_obj|
assoc_obj_ident = object_ident.call assoc_obj
edge_list[this_object_ident].push assoc_obj_ident
next if edge_list[assoc_obj_ident] #if we've already seen it, skip it
unless @prune_nodes.include? assoc_obj.class.to_s.underscore #if it's on the prune list, add it, but don't recurse
traverse.call(assoc_obj)
else
fill_object_data.call assoc_obj, assoc_obj_ident
end
end
end
end
traverse.call(root_object)
@json = {rootId:object_ident.call(root_object), edges:edge_list, nodes:nodes}.to_json
render '/application/object_graph' #keep the view in a common folder
end
#using the [VivaGraph library](https://github.com/anvaka/VivaGraphJS) to build a nice looking graph
#this examples requires Underscore.js and the jQuery-ui theme roller icons
ObjectGraph = (rootId, nodes, nodeLinks)->
graph = Viva.Graph.graph()
_.each nodes, (node, node_id)->
graph.addNode node_id, node
_.each nodeLinks, (nodeLinkList, key)->
_.each nodeLinkList, (nodeLink)->
graph.addLink(key, nodeLink)
graphics = Viva.Graph.View.svgGraphics()
graphics.node (node)-> #build the node element from SVG and HTML components
fillColor = if node.data.name == rootId then 'red' else 'blue'
svg = Viva.Graph.svg('svg')
g = svg.appendChild Viva.Graph.svg('g')
g.appendChild Viva.Graph.svg('rect').attr('width', 10).attr('height', 10).attr('fill', fillColor)
#foreign element sizeing could be done a lot better than what I have here
foreignOject = g.appendChild Viva.Graph.svg('foreignObject').attr('width', '13em').attr('height', '2em').attr('fill', fillColor)
body = foreignOject.appendChild document.createElementNS('http://www.w3.org/1999/xhtml', 'body')
div = body.appendChild document.createElement('div')
div.appendChild(document.createElement 'span' ).textContent = node.data.type
div2 = body.appendChild document.createElement('div')
div2.appendChild(document.createElement 'span').textContent = node.data.description
if node.data.href
a = div.appendChild document.createElement('a')
a.setAttribute('href', node.data.href)
linkIcon = a.appendChild document.createElement('span')
linkIcon.setAttribute('class', "ui-icon ui-icon-link")
linkIcon.setAttribute('style', "display:inline-block") #so icon and text are on the same line, using the style sheet didn't work for me
svg
renderer = Viva.Graph.View.renderer graph, {graphics:graphics, container:document.getElementById('your_ele_id')}
renderer.run()
<% if @prune_nodes %>
<ul class='inline'>
<li>Network Pruned Nodes:</li>
<% @prune_nodes.each do |prune|%>
<li><%= raw prune.titleize %> </li>
<% end %>
</ul>
<% end %>
<script type="text/javascript">
json = <%= raw @json %>
ObjectGraph(json.rootId, json.nodes, json.edges);
</script>
<style type="text/css">
svg{ overflow:visible !important; } /*easier than trying to size the SVG element right*/
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment