neo-viz 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. data/.gitignore +9 -0
  2. data/.livereload +20 -0
  3. data/.rspec +2 -0
  4. data/.rvmrc +3 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +19 -0
  7. data/README.md +120 -0
  8. data/Rakefile +12 -0
  9. data/bin/neo-viz +12 -0
  10. data/config.ru +35 -0
  11. data/lib/neo-viz.rb +185 -0
  12. data/lib/neo-viz/version.rb +6 -0
  13. data/neo-viz.gemspec +37 -0
  14. data/public/coffeescripts/app_context.coffee +89 -0
  15. data/public/coffeescripts/canvas_util.coffee +68 -0
  16. data/public/coffeescripts/event_broker.coffee +16 -0
  17. data/public/coffeescripts/filters.coffee +113 -0
  18. data/public/coffeescripts/main.coffee.erb +132 -0
  19. data/public/coffeescripts/neo4j.coffee +90 -0
  20. data/public/coffeescripts/renderer.coffee +141 -0
  21. data/public/coffeescripts/space.coffee +81 -0
  22. data/public/images/ajax-loader.gif +0 -0
  23. data/public/javascripts/data.js +45 -0
  24. data/public/javascripts/data2.js +1287 -0
  25. data/public/javascripts/main.sprockets.js +9 -0
  26. data/public/lib/arbor/arbor-tween.js +86 -0
  27. data/public/lib/arbor/arbor.js +67 -0
  28. data/public/lib/jQuery/jquery-1.6.1.min.js +18 -0
  29. data/public/lib/jasmine-1.1.0/MIT.LICENSE +20 -0
  30. data/public/lib/jasmine-1.1.0/jasmine-html.js +190 -0
  31. data/public/lib/jasmine-1.1.0/jasmine.css +166 -0
  32. data/public/lib/jasmine-1.1.0/jasmine.js +2476 -0
  33. data/public/lib/jasmine-1.1.0/jasmine_favicon.png +0 -0
  34. data/public/lib/stdlib/stdlib.js +115 -0
  35. data/public/lib/sylvester-0.1.3/CHANGELOG.txt +29 -0
  36. data/public/lib/sylvester-0.1.3/sylvester.js +1 -0
  37. data/public/lib/sylvester-0.1.3/sylvester.js.gz +0 -0
  38. data/public/lib/sylvester-0.1.3/sylvester.src.js +1254 -0
  39. data/public/scss/main.scss +152 -0
  40. data/public/scss/mixins.scss +37 -0
  41. data/spec/coffeescripts/canvas_util_spec.coffee +4 -0
  42. data/spec/coffeescripts/filters_spec.coffee +37 -0
  43. data/spec/coffeescripts/neo4j_spec.coffee +76 -0
  44. data/spec/neo_viz_spec.rb +48 -0
  45. data/spec/spec_helper.rb +17 -0
  46. data/spec/support/struct_matcher.rb +117 -0
  47. data/views/_filters.haml +22 -0
  48. data/views/_partial.haml +39 -0
  49. data/views/embedded.haml +15 -0
  50. data/views/index.haml +19 -0
  51. data/views/jasmine_specs_runner.haml +50 -0
  52. metadata +236 -0
data/neo-viz.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "neo-viz/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "neo-viz"
7
+ s.version = Neo::Viz::VERSION
8
+ s.authors = ["Anders Janmyr"]
9
+ s.email = ["anders.janmyr@jayway.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{A gem for visualizing a Neo database with Javascript}
12
+ s.description = %q{A gem for visualizing a Neo database with Javascript}
13
+
14
+ s.rubyforge_project = "neo-viz"
15
+
16
+ s.add_dependency 'sprockets'
17
+ s.add_dependency 'sinatra'
18
+ s.add_dependency 'sinatra-reloader'
19
+ s.add_dependency 'neo4j', '~> 1.1'
20
+ s.add_dependency 'coffee-script'
21
+ s.add_dependency 'haml'
22
+ s.add_dependency 'sass'
23
+ s.add_dependency 'json'
24
+ s.add_dependency 'pry'
25
+
26
+
27
+ s.add_development_dependency 'rspec'
28
+ s.add_development_dependency 'rack-test'
29
+ s.add_development_dependency 'jasmine'
30
+
31
+ s.requirements << 'coffee-script'
32
+
33
+ s.files = `git ls-files`.split("\n")
34
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
35
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
36
+ s.require_paths = ["lib"]
37
+ end
@@ -0,0 +1,89 @@
1
+ $ = jQuery
2
+
3
+ class AppContext
4
+
5
+ constructor: (@eventBroker)->
6
+ @nodeFilter = ''
7
+ @keyFilter = ''
8
+ @nodeData = null
9
+ @nodeCount = 10
10
+ @activatedNodeId = null
11
+ @selectedObject = {"id":0, "kind":""}
12
+
13
+ setNodeCount: (n) ->
14
+ if (@nodeCount != n)
15
+ @nodeCount = n
16
+ @publish("nodeCountChanged")
17
+
18
+ getNodeCount: () ->
19
+ @nodeCount
20
+
21
+ setKeyFilter: (keyFilter) ->
22
+ if (@keyFilter != keyFilter)
23
+ @keyFilter = keyFilter
24
+ @publish("keyFilterChanged")
25
+
26
+ getKeyFilter: ->
27
+ @keyFilter
28
+
29
+ setNodeFilter: (filter) ->
30
+ if (@nodeFilter != filter)
31
+ @nodeFilter = filter
32
+ @publish("nodeFilterChanged")
33
+
34
+ getNodeFilter: ->
35
+ @nodeFilter
36
+
37
+ setActivatedNodeId: (nodeId, suppressChangedEvent=false) ->
38
+ if (@activatedNodeId != nodeId)
39
+ @activatedNodeId = nodeId
40
+ if (!suppressChangedEvent)
41
+ @publish("activatedNodeIdChanged")
42
+
43
+ getActivatedNodeId: ->
44
+ @activatedNodeId
45
+
46
+ setSelectedObject: (id, kind) ->
47
+ if (@selectedObjectId != id && @selectedObjectKind != kind)
48
+ @selectedObject.id = id
49
+ @selectedObject.kind = kind
50
+ @publish("selectedObjectChanged")
51
+
52
+ getSelectedObject: ->
53
+ @selectedObject
54
+
55
+
56
+ # I.e. nodeData.nodes, nodeData.rels
57
+ setNodeData: (nodeData) ->
58
+ if (@nodeData != nodeData)
59
+ @nodeData = nodeData
60
+ @graph = new Graph(nodeData.nodes, nodeData.rels)
61
+ @publish("nodeDataChanged")
62
+
63
+ getNodeData: ->
64
+ @nodeData
65
+
66
+ getGraph: ->
67
+ @graph
68
+
69
+ # I.e. hiddenNodeData.nodeIds, hiddenNodeData.relIds
70
+ setHiddenNodeData: (hiddenNodeData) ->
71
+ if (@hiddenNodeData != hiddenNodeData)
72
+ @hiddenNodeData = hiddenNodeData
73
+ @publish("hiddenNodeDataChanged")
74
+
75
+ getHiddenNodeData: ->
76
+ @hiddenNodeData
77
+
78
+ clearHiddenNodeData: ->
79
+ @hiddenNodeData = {nodeIds:[], relIds:[]}
80
+ @publish("hiddenNodeDataChanged")
81
+
82
+ # TODO: How do we make this a private method?
83
+ publish: (eventName) ->
84
+ @eventBroker.publish(eventName)
85
+
86
+ $ ->
87
+ root = exports ? this
88
+ root.appContext = new AppContext(root.eventBroker)
89
+
@@ -0,0 +1,68 @@
1
+ root = exports ? window
2
+
3
+ root.CanvasUtil =
4
+ centerToEdge: (val, delta) ->
5
+ val - delta/2
6
+
7
+ roundRect: (ctx, point, width, height, color, radius=5) ->
8
+ x = @centerToEdge(point.x, width)
9
+ y = @centerToEdge(point.y, height)
10
+ ctx.beginPath()
11
+ ctx.moveTo(x + radius, y)
12
+ ctx.fillStyle = @createGradient(ctx, color, y, height)
13
+ ctx.lineTo(x + width - radius, y)
14
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius)
15
+ ctx.lineTo(x + width, y + height - radius)
16
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
17
+ ctx.lineTo(x + radius, y + height)
18
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius)
19
+ ctx.lineTo(x, y + radius)
20
+ ctx.quadraticCurveTo(x, y, x + radius, y)
21
+ ctx.closePath()
22
+ ctx.fill()
23
+
24
+ line: (ctx, fromPoint, toPoint, width = 2) ->
25
+ ctx.lineWidth = width
26
+ ctx.beginPath()
27
+ ctx.moveTo fromPoint.x, fromPoint.y
28
+ ctx.lineTo toPoint.x, toPoint.y
29
+ @arrow(ctx, fromPoint, toPoint, width)
30
+ ctx.stroke()
31
+
32
+ arrow: (ctx, fromPoint, toPoint, width = 2) ->
33
+ ctx.save()
34
+ mx = (toPoint.x + fromPoint.x) / 2
35
+ my = (toPoint.y + fromPoint.y) / 2
36
+
37
+ ctx.translate(mx, my)
38
+ # draw your arrow, with its origin at [0, 0]
39
+ angle = Math.atan2(toPoint.y-fromPoint.y, toPoint.x-fromPoint.x)
40
+ ctx.rotate(angle)
41
+ arrowSize = 6
42
+ ctx.moveTo(0, 0)
43
+ ctx.lineTo(-arrowSize, -arrowSize)
44
+ ctx.moveTo(0, 0)
45
+ ctx.lineTo(-arrowSize, arrowSize)
46
+ ctx.restore()
47
+
48
+ textSize: (ctx, text) ->
49
+ lineHeight = ctx.measureText(text[0]).height or 16
50
+ width = 0
51
+ count = text.length
52
+ height = count * lineHeight + 20
53
+ for line in text
54
+ width = Math.max ctx.measureText(line).width, width
55
+ {width, height, count, lineHeight }
56
+
57
+ drawText: (ctx, text, left, top) ->
58
+ textSize = @textSize(ctx, text)
59
+ for i in [0...textSize.count]
60
+ line = text[i]
61
+ ctx.fillText(line, left, top + i*textSize.lineHeight)
62
+
63
+ createGradient: (ctx, color, y, height) ->
64
+ gradient = ctx.createLinearGradient(0, y, 0, y+height+40)
65
+ gradient.addColorStop(0, color)
66
+ gradient.addColorStop(1, "white")
67
+ gradient
68
+
@@ -0,0 +1,16 @@
1
+ $ = jQuery
2
+
3
+ class EventBroker
4
+
5
+ publish: (eventName) ->
6
+ $("body").trigger(eventName)
7
+ console.log 'published ' + eventName
8
+
9
+ subscribe: (eventName, func) ->
10
+ $("body").bind(eventName, () ->
11
+ func()
12
+ )
13
+
14
+ $ ->
15
+ root = exports ? this
16
+ root.eventBroker = new EventBroker
@@ -0,0 +1,113 @@
1
+ $ = jQuery
2
+
3
+ initFormListeners = (appContext, eventBroker) ->
4
+ $('#node-count').change ->
5
+ appContext.setNodeCount($(this).val())
6
+ eventBroker.publish('refresh')
7
+
8
+ $('#node-filter').change ->
9
+ appContext.setNodeFilter($(this).val())
10
+ eventBroker.publish('refresh')
11
+
12
+ $('#key-filter').change ->
13
+ appContext.setKeyFilter($(this).val())
14
+ eventBroker.publish('refresh')
15
+
16
+ initSubscribers = (appContext, eventBroker) ->
17
+ eventBroker.subscribe('nodeDataChanged', ->
18
+ refreshRelationFilters(appContext)
19
+ )
20
+
21
+ refreshRelationFilters = (appContext)->
22
+ activatedNodeId = appContext.getActivatedNodeId()
23
+ return if (activatedNodeId == null)
24
+
25
+ graph = appContext.getGraph()
26
+ activatedNode = graph.load(activatedNodeId)
27
+ # Event timing issue: when reloading data we have not
28
+ # set the new activatedNodeId yet so we might try to fetch non-existent node
29
+ # here.
30
+ return if (activatedNode == null)
31
+
32
+ incomingTypes = (rel.type for rel in activatedNode.incoming()).unique()
33
+ outgoingTypes = (rel.type for rel in activatedNode.outgoing()).unique()
34
+ allRelTypes = incomingTypes.union(outgoingTypes).sort()
35
+
36
+ $('#relationsFilterTable').empty()
37
+
38
+ for relType in allRelTypes
39
+ hasIncoming = incomingTypes.contains(relType)
40
+ hasOutgoing = outgoingTypes.contains(relType)
41
+ inCheckboxHtml = buildCheckboxHtml relType, "in", hasIncoming
42
+ outCheckboxHtml = buildCheckboxHtml relType, "out", hasOutgoing
43
+
44
+ $('#relationsFilterTable').append("<tr><td>#{relType} #{inCheckboxHtml}</td><td>#{outCheckboxHtml}</td></tr>")
45
+
46
+ $("#relationsFilterTable input").change ->
47
+ updateHiddenNodeData(appContext, graph, activatedNode)
48
+
49
+ updateHiddenNodeData = (appContext, graph, activatedNode) ->
50
+ relsToHide = ({ type:"#{checkbox.name}", direction:"#{checkbox.value}"} for checkbox in $("#relationsFilterTable input") when (!checkbox.disabled && !checkbox.checked))
51
+
52
+ incomingTypesToHide = (rel.type for rel in relsToHide when rel.direction == "in")
53
+ outgoingTypesToHide = (rel.type for rel in relsToHide when rel.direction == "out")
54
+
55
+ relsHiddenByUser = activatedNode.incoming(incomingTypesToHide)
56
+ relsHiddenByUser = relsHiddenByUser.concat(activatedNode.outgoing(outgoingTypesToHide))
57
+
58
+ hiddenNodeData = buildHiddenNodeData graph, activatedNode, relsHiddenByUser
59
+
60
+ appContext.setHiddenNodeData(hiddenNodeData)
61
+
62
+ buildHiddenNodeData = (graph, activatedNode, relsHiddenByUser) ->
63
+
64
+ hiddenNodeData = nodeIds:[], relIds:(rel.id for rel in relsHiddenByUser)
65
+
66
+ allRels = graph.relationships
67
+ activeRels = allRels.diff(relsHiddenByUser)
68
+ for rel in activatedNode.both()
69
+ if relsHiddenByUser.contains(rel)
70
+ otherNode = rel.other(activatedNode)
71
+
72
+ if (!graph.areConnected(activatedNode, otherNode, activeRels))
73
+ #console.log "node " + activatedNode.id + " and " + otherNode.id + " are not connected"
74
+ # No connections to otherNode exists, so hide otherNode and its subgraph
75
+ appendHiddenNodeDataForSubGraph(otherNode, activeRels, hiddenNodeData)
76
+
77
+ hiddenNodeData
78
+
79
+ appendHiddenNodeDataForSubGraph = (node, mutableActiveRels, hiddenNodeData) -> #={nodeIds: [], relIds: []}) ->
80
+
81
+ hiddenNodeData.nodeIds.push(node.id)
82
+ for rel in node.both()
83
+ if mutableActiveRels.contains(rel)
84
+ hiddenNodeData.relIds.push(rel.id)
85
+ # Remove the relationship already traversed so that
86
+ # next iteration does not traverse "backwards" again
87
+ mutableActiveRels.remove(mutableActiveRels.indexOf(rel))
88
+ other = rel.other(node)
89
+ # Have we already hidden the other node?
90
+ if (!hiddenNodeData.nodeIds.contains(other.id))
91
+ # No, go ahead and hide its subgraph.
92
+ appendHiddenNodeDataForSubGraph(other, mutableActiveRels, hiddenNodeData)
93
+
94
+
95
+ buildCheckboxHtml = (relType, value, enabled) ->
96
+ html = "<input type='checkbox' name=\"#{relType}\" value='#{value}'"
97
+ html += " checked='true'" if enabled
98
+ html += " disabled='disabled'" if !enabled
99
+ html += " />"
100
+ html += "<del>" if !enabled
101
+ html += value
102
+ html += "</del>" if !enabled
103
+ html
104
+
105
+
106
+ root = exports ? this
107
+ root.test_buildHiddenNodeData = buildHiddenNodeData # global for unit testing
108
+
109
+
110
+ $ ->
111
+
112
+ initSubscribers(@appContext, @eventBroker)
113
+ initFormListeners(@appContext, @eventBroker)
@@ -0,0 +1,132 @@
1
+ //= require 'renderer.coffee'
2
+ //= require 'space.coffee'
3
+
4
+ $ = jQuery
5
+
6
+ initFormListeners= (space, renderer, evalCode) ->
7
+ $('#loadForm').submit (e) ->
8
+ e.preventDefault()
9
+ console.log 'load'
10
+ evalCode()
11
+
12
+ showNodeDetails = (space, id=0) =>
13
+ node = space.node(id)
14
+ return unless node
15
+ showDetails(node.data)
16
+
17
+ showDetails = (data) =>
18
+ html = for key, value of data
19
+ if key is 'first' then '' else "<tr><td>#{key}</td><td>#{value}</td></tr>"
20
+ $('#details').empty().append(html.join('\n'))
21
+
22
+ showEdgeDetails = (space, id=0) =>
23
+ edge = space.rel(id)
24
+ return unless edge
25
+ showDetails(edge.data)
26
+
27
+
28
+ initEventSubscribers = (eventBroker, appContext, space, renderer, getData) ->
29
+ eventBroker.subscribe('nodeCountChanged', ->
30
+ space.setNodeCount appContext.getNodeCount()
31
+ )
32
+
33
+ eventBroker.subscribe('nodeFilterChanged', ->
34
+ space.setFilter appContext.getNodeFilter()
35
+ )
36
+
37
+ eventBroker.subscribe('keyFilterChanged', ->
38
+ renderer.setKeyFilter appContext.getKeyFilter()
39
+ )
40
+
41
+ eventBroker.subscribe('nodeDataChanged', ->
42
+ appContext.clearHiddenNodeData()
43
+ nodeData = appContext.getNodeData()
44
+ space.addData nodeData
45
+ if nodeData.nodes.length > 0
46
+ firstNode = nodeData.nodes[0]
47
+ appContext.setActivatedNodeId(firstNode.id)
48
+ appContext.setSelectedObject(firstNode.id, "node")
49
+ )
50
+
51
+ eventBroker.subscribe('hiddenNodeDataChanged', ->
52
+ space.setHiddenData appContext.getHiddenNodeData()
53
+ )
54
+
55
+ eventBroker.subscribe('activatedNodeIdChanged', ->
56
+ getData appContext.getActivatedNodeId()
57
+ )
58
+
59
+ eventBroker.subscribe('selectedObjectChanged', ->
60
+ object = appContext.getSelectedObject()
61
+ if (object.kind == "node")
62
+ showNodeDetails(space, object.id)
63
+ else
64
+ showEdgeDetails(space, object.id)
65
+
66
+ )
67
+
68
+ eventBroker.subscribe('refresh', ->
69
+ space.refresh()
70
+ )
71
+
72
+ $ ->
73
+
74
+ $('#consoleOutput').hide()
75
+
76
+ sys = arbor.ParticleSystem(1000, 600, 0.5) # create the system with sensible repulsion/stiffness/friction
77
+ sys.parameters({gravity:true}) # use center-gravity to make the graph settle nicely (ymmv)
78
+
79
+ space = new Space(sys, @appContext.getNodeCount())
80
+
81
+ appendToConsole = (text) ->
82
+ $('#console').val($('#console').val()+ '\n#' + text)
83
+
84
+ setError = (text) ->
85
+ $('#consoleOutputArea').val(text)
86
+
87
+ getData = (id=0) =>
88
+ depth = $('#depth').val()
89
+ code = if id is 0 then "node = Neo4j.ref_node" else "node = Node._load(#{id})"
90
+ code += "; viz node"
91
+ innerEvalCode(code, depth)
92
+
93
+ evalCode = =>
94
+ code = $('#console').val()
95
+ depth = $('#depth').val()
96
+ innerEvalCode(code, depth)
97
+
98
+ innerEvalCode = (code, depth) =>
99
+ appContext = @appContext
100
+ console.log code
101
+ $('#eval').attr('disabled', 'true')
102
+ $('#ajax-loader').show()
103
+ $.getJSON "<%= root_url() %>/eval", {code: code, depth: depth}, (data) ->
104
+ $('#ajax-loader').hide()
105
+ $('#eval').removeAttr('disabled')
106
+ if data.result
107
+ $('#consoleOutput').show()
108
+ setError data.result
109
+ else
110
+ setError ""
111
+ $('#consoleOutput').hide()
112
+ appContext.setNodeData data
113
+
114
+ activateNode = (id=0) =>
115
+ @appContext.setActivatedNodeId(id)
116
+
117
+ selectNode = (id=0) =>
118
+ @appContext.setSelectedObject(id, "node")
119
+
120
+ selectEdge = (id=0) =>
121
+ @appContext.setSelectedObject(id, "edge")
122
+
123
+ objectHandler =
124
+ activated: activateNode
125
+ selectedNode: selectNode
126
+ selectedEdge: selectEdge
127
+
128
+ sys.renderer = Renderer("#viewport", objectHandler)
129
+
130
+ initFormListeners(space, sys.renderer, evalCode)
131
+ initEventSubscribers(@eventBroker, @appContext, space, sys.renderer, getData)
132
+ activateNode(0)