model-visualizer 0.0.2 → 1.0.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f85b56d9e73dfedbe9dddf499329ece11391694b
4
- data.tar.gz: 99ba57a781f2c7f6b65a0e1161c4b70608795912
3
+ metadata.gz: bfe6c500461c09bfa2290ee696b56c6b49a3964d
4
+ data.tar.gz: 2c0dc9fbfee71a9867578da437e3b1b1951f5566
5
5
  SHA512:
6
- metadata.gz: bdc68954e9628bcc93ffa680e19a4252d45bb45e1fa3d2225ce63b86ec3ed2c39268de18b38d80a13ef490e836534a061b454e906d8be477a7fcde52800a9b97
7
- data.tar.gz: ce79b5640c79d4eb2ada3a5cdbe61616f825dbc47d4f8d2af4e8fe571d24098aa96489d8ca28718306446142ea1cfed8218dc3330fa48886bbadff29f9262c26
6
+ metadata.gz: d0901702b651f3acf807555d6b05e1ef73c891c4c4fbe391854fe2dcac119bfae27cfb4030d59dd6ff269a5b4a5c86f8be09a5d58baeff5002959a8836b7b770
7
+ data.tar.gz: 9f7e0d5b40f77643bce5ba1d394d88985e9f6902d337f1ad1f9784fa333c7369706cb5f182b95f25e793731132ff6e5c67c92bd04c042d50362757ff7826d65f
@@ -14,6 +14,6 @@ class ModelVisualizer
14
14
  root = if ARGV.length == 1 then ARGV[0] else '.' end
15
15
 
16
16
  models = Parser.parse root
17
- Visualizer.new(models).create_visualization
17
+ Visualizer.new(models).create_visualization(File.basename root)
18
18
  end
19
19
  end
@@ -1,23 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  class Model
4
- attr_reader :belongs_to
5
- attr_reader :has_one
6
- attr_reader :has_many
7
- attr_reader :has_and_belongs_to_many
8
- attr_reader :integer_attributes
9
- attr_reader :string_attributes
10
- attr_reader :primary_key_attributes
11
- attr_reader :text_attributes
12
- attr_reader :float_attributes
13
- attr_reader :decimal_attributes
14
- attr_reader :datetime_attributes
15
- attr_reader :timestamp_attributes
16
- attr_reader :time_attributes
17
- attr_reader :date_attributes
18
- attr_reader :binary_attributes
19
- attr_reader :boolean_attributes
20
- attr_reader :foreign_keys
21
4
 
22
5
  def initialize(name)
23
6
  @name = name
@@ -13,7 +13,8 @@ class SchemaParser
13
13
  def parse(root)
14
14
  file = File.join(root, SCHEMA_FILE)
15
15
  unless File.file? file
16
- abort 'db/schema.rb does not exist! Run from or pass the root directory of your Rails project.'
16
+ puts 'db/schema.rb does not exist! If this is surprising, run the command again from the Rails project directory.'
17
+ return
17
18
  end
18
19
 
19
20
  curr_model = nil
@@ -9,22 +9,37 @@ class Visualizer
9
9
  @models = models
10
10
  end
11
11
 
12
- def create_visualization
12
+ def create_visualization(title)
13
13
  # Get file from gem directory
14
14
  g = Gem::Specification.find_by_name 'model-visualizer'
15
15
  template = File.join(g.full_gem_path, 'share/template.html')
16
+ css = File.join(g.full_gem_path, 'share/main.css')
16
17
  d3 = File.join(g.full_gem_path, 'share/d3.min.js')
18
+ tooltip = File.join(g.full_gem_path, 'share/d3.tip.js')
17
19
 
18
20
  # Insert data into file
19
21
  template_contents = File.read template
20
22
  output = template_contents.gsub(/<%= @models %>/, JSON.generate(@models))
23
+ .gsub(/<%= @css %>/, css)
21
24
  .gsub(/<%= @d3 %>/, d3)
25
+ .gsub(/<%= @title %>/, title + ' Model Visualization')
26
+ .gsub(/<%= @sidebar %>/, create_sidebar)
27
+ .gsub(/<%= @tooltip %>/, tooltip)
22
28
 
23
29
  # Write and open file
24
30
  File.open(FILE_PATH, 'w') {|file| file.puts output}
25
31
  self.launch_browser FILE_PATH
26
32
  end
27
33
 
34
+ def create_sidebar
35
+ str = '<div class="sidebar">'
36
+ str += '<div class="search"><input type="search" class="searchbox" results=5 size="large" placeholder="Search"></div>' # input type="search" does not let you resize in webkit
37
+ @models.sort.each do |name, model|
38
+ str += '<div class="model" onclick="highlightNode(this.innerHTML)">' + name + '</div>'
39
+ end
40
+ str += '</div>'
41
+ end
42
+
28
43
  # http://stackoverflow.com/questions/152699/open-the-default-browser-in-ruby
29
44
  def launch_browser(path)
30
45
  if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
Binary file
@@ -0,0 +1,325 @@
1
+ // d3.tip
2
+ // Copyright (c) 2013 Justin Palmer
3
+ //
4
+ // Tooltips for d3.js SVG visualizations
5
+
6
+ (function (root, factory) {
7
+ if (typeof define === 'function' && define.amd) {
8
+ // AMD. Register as an anonymous module with d3 as a dependency.
9
+ define(['d3'], factory)
10
+ } else if (typeof module === 'object' && module.exports) {
11
+ // CommonJS
12
+ module.exports = function(d3) {
13
+ d3.tip = factory(d3)
14
+ return d3.tip
15
+ }
16
+ } else {
17
+ // Browser global.
18
+ root.d3.tip = factory(root.d3)
19
+ }
20
+ }(this, function (d3) {
21
+
22
+ // Public - contructs a new tooltip
23
+ //
24
+ // Returns a tip
25
+ return function() {
26
+ var direction = d3_tip_direction,
27
+ offset = d3_tip_offset,
28
+ html = d3_tip_html,
29
+ node = initNode(),
30
+ svg = null,
31
+ point = null,
32
+ target = null
33
+
34
+ function tip(vis) {
35
+ svg = getSVGNode(vis)
36
+ point = svg.createSVGPoint()
37
+ document.body.appendChild(node)
38
+ }
39
+
40
+ // Public - show the tooltip on the screen
41
+ //
42
+ // Returns a tip
43
+ tip.show = function() {
44
+ var args = Array.prototype.slice.call(arguments)
45
+ if(args[args.length - 1] instanceof SVGElement) target = args.pop()
46
+
47
+ var content = html.apply(this, args),
48
+ poffset = offset.apply(this, args),
49
+ dir = direction.apply(this, args),
50
+ nodel = getNodeEl(),
51
+ i = directions.length,
52
+ coords,
53
+ scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
54
+ scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
55
+
56
+ nodel.html(content)
57
+ .style({ opacity: 1, 'pointer-events': 'all' })
58
+
59
+ while(i--) nodel.classed(directions[i], false)
60
+ coords = direction_callbacks.get(dir).apply(this)
61
+ // changed this to 0, 0 to put tip in top right corner
62
+ nodel.classed(dir, true).style({
63
+ top: 0,
64
+ right: 0
65
+ })
66
+
67
+ return tip
68
+ }
69
+
70
+ // Public - hide the tooltip
71
+ //
72
+ // Returns a tip
73
+ tip.hide = function() {
74
+ var nodel = getNodeEl()
75
+ nodel.style({ opacity: 0, 'pointer-events': 'none' })
76
+ return tip
77
+ }
78
+
79
+ // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
80
+ //
81
+ // n - name of the attribute
82
+ // v - value of the attribute
83
+ //
84
+ // Returns tip or attribute value
85
+ tip.attr = function(n, v) {
86
+ if (arguments.length < 2 && typeof n === 'string') {
87
+ return getNodeEl().attr(n)
88
+ } else {
89
+ var args = Array.prototype.slice.call(arguments)
90
+ d3.selection.prototype.attr.apply(getNodeEl(), args)
91
+ }
92
+
93
+ return tip
94
+ }
95
+
96
+ // Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
97
+ //
98
+ // n - name of the property
99
+ // v - value of the property
100
+ //
101
+ // Returns tip or style property value
102
+ tip.style = function(n, v) {
103
+ if (arguments.length < 2 && typeof n === 'string') {
104
+ return getNodeEl().style(n)
105
+ } else {
106
+ var args = Array.prototype.slice.call(arguments)
107
+ d3.selection.prototype.style.apply(getNodeEl(), args)
108
+ }
109
+
110
+ return tip
111
+ }
112
+
113
+ // Public: Set or get the direction of the tooltip
114
+ //
115
+ // v - One of n(north), s(south), e(east), or w(west), nw(northwest),
116
+ // sw(southwest), ne(northeast) or se(southeast)
117
+ //
118
+ // Returns tip or direction
119
+ tip.direction = function(v) {
120
+ if (!arguments.length) return direction
121
+ direction = v == null ? v : d3.functor(v)
122
+
123
+ return tip
124
+ }
125
+
126
+ // Public: Sets or gets the offset of the tip
127
+ //
128
+ // v - Array of [x, y] offset
129
+ //
130
+ // Returns offset or
131
+ tip.offset = function(v) {
132
+ if (!arguments.length) return offset
133
+ offset = v == null ? v : d3.functor(v)
134
+
135
+ return tip
136
+ }
137
+
138
+ // Public: sets or gets the html value of the tooltip
139
+ //
140
+ // v - String value of the tip
141
+ //
142
+ // Returns html value or tip
143
+ tip.html = function(v) {
144
+ if (!arguments.length) return html
145
+ html = v == null ? v : d3.functor(v)
146
+
147
+ return tip
148
+ }
149
+
150
+ // Public: destroys the tooltip and removes it from the DOM
151
+ //
152
+ // Returns a tip
153
+ tip.destroy = function() {
154
+ if(node) {
155
+ getNodeEl().remove();
156
+ node = null;
157
+ }
158
+ return tip;
159
+ }
160
+
161
+ function d3_tip_direction() { return 'n' }
162
+ function d3_tip_offset() { return [0, 0] }
163
+ function d3_tip_html() { return ' ' }
164
+
165
+ var direction_callbacks = d3.map({
166
+ n: direction_n,
167
+ s: direction_s,
168
+ e: direction_e,
169
+ w: direction_w,
170
+ nw: direction_nw,
171
+ ne: direction_ne,
172
+ sw: direction_sw,
173
+ se: direction_se
174
+ }),
175
+
176
+ directions = direction_callbacks.keys()
177
+
178
+ function direction_n() {
179
+ var bbox = getScreenBBox()
180
+ return {
181
+ top: bbox.n.y - node.offsetHeight,
182
+ left: bbox.n.x - node.offsetWidth / 2
183
+ }
184
+ }
185
+
186
+ function direction_s() {
187
+ var bbox = getScreenBBox()
188
+ return {
189
+ top: bbox.s.y,
190
+ left: bbox.s.x - node.offsetWidth / 2
191
+ }
192
+ }
193
+
194
+ function direction_e() {
195
+ var bbox = getScreenBBox()
196
+ return {
197
+ top: bbox.e.y - node.offsetHeight / 2,
198
+ left: bbox.e.x
199
+ }
200
+ }
201
+
202
+ function direction_w() {
203
+ var bbox = getScreenBBox()
204
+ return {
205
+ top: bbox.w.y - node.offsetHeight / 2,
206
+ left: bbox.w.x - node.offsetWidth
207
+ }
208
+ }
209
+
210
+ function direction_nw() {
211
+ var bbox = getScreenBBox()
212
+ return {
213
+ top: bbox.nw.y - node.offsetHeight,
214
+ left: bbox.nw.x - node.offsetWidth
215
+ }
216
+ }
217
+
218
+ function direction_ne() {
219
+ var bbox = getScreenBBox()
220
+ return {
221
+ top: bbox.ne.y - node.offsetHeight,
222
+ left: bbox.ne.x
223
+ }
224
+ }
225
+
226
+ function direction_sw() {
227
+ var bbox = getScreenBBox()
228
+ return {
229
+ top: bbox.sw.y,
230
+ left: bbox.sw.x - node.offsetWidth
231
+ }
232
+ }
233
+
234
+ function direction_se() {
235
+ var bbox = getScreenBBox()
236
+ return {
237
+ top: bbox.se.y,
238
+ left: bbox.e.x
239
+ }
240
+ }
241
+
242
+ function initNode() {
243
+ var node = d3.select(document.createElement('div'))
244
+ node.style({
245
+ position: 'absolute',
246
+ top: 0,
247
+ opacity: 0,
248
+ 'pointer-events': 'none',
249
+ 'box-sizing': 'border-box'
250
+ })
251
+
252
+ return node.node()
253
+ }
254
+
255
+ function getSVGNode(el) {
256
+ el = el.node()
257
+ if(el.tagName.toLowerCase() === 'svg')
258
+ return el
259
+
260
+ return el.ownerSVGElement
261
+ }
262
+
263
+ function getNodeEl() {
264
+ if(node === null) {
265
+ node = initNode();
266
+ // re-add node to DOM
267
+ document.body.appendChild(node);
268
+ };
269
+ return d3.select(node);
270
+ }
271
+
272
+ // Private - gets the screen coordinates of a shape
273
+ //
274
+ // Given a shape on the screen, will return an SVGPoint for the directions
275
+ // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
276
+ // sw(southwest).
277
+ //
278
+ // +-+-+
279
+ // | |
280
+ // + +
281
+ // | |
282
+ // +-+-+
283
+ //
284
+ // Returns an Object {n, s, e, w, nw, sw, ne, se}
285
+ function getScreenBBox() {
286
+ var targetel = target || d3.event.target;
287
+
288
+ while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
289
+ targetel = targetel.parentNode;
290
+ }
291
+
292
+ var bbox = {},
293
+ matrix = targetel.getScreenCTM(),
294
+ tbbox = targetel.getBBox(),
295
+ width = tbbox.width,
296
+ height = tbbox.height,
297
+ x = tbbox.x,
298
+ y = tbbox.y
299
+
300
+ point.x = x
301
+ point.y = y
302
+ bbox.nw = point.matrixTransform(matrix)
303
+ point.x += width
304
+ bbox.ne = point.matrixTransform(matrix)
305
+ point.y += height
306
+ bbox.se = point.matrixTransform(matrix)
307
+ point.x -= width
308
+ bbox.sw = point.matrixTransform(matrix)
309
+ point.y -= height / 2
310
+ bbox.w = point.matrixTransform(matrix)
311
+ point.x += width
312
+ bbox.e = point.matrixTransform(matrix)
313
+ point.x -= width / 2
314
+ point.y -= height / 2
315
+ bbox.n = point.matrixTransform(matrix)
316
+ point.y += height
317
+ bbox.s = point.matrixTransform(matrix)
318
+
319
+ return bbox
320
+ }
321
+
322
+ return tip
323
+ };
324
+
325
+ }));
@@ -0,0 +1,140 @@
1
+ @font-face {
2
+ font-family: 'Open Sans';
3
+ src: url('./OpenSans-Regular.ttf');
4
+ }
5
+
6
+ html, body {
7
+ height: 100%;
8
+ width: 100%;
9
+ overflow: hidden;
10
+ }
11
+
12
+ body {
13
+ background-image: url("background.png");
14
+ font-family: 'Open Sans', sans-serif;
15
+ margin: 0;
16
+ top: 0;
17
+ }
18
+
19
+ .sidebar {
20
+ position: fixed;
21
+ left: 0;
22
+ top: 0;
23
+ height: 100%;
24
+ overflow: scroll;
25
+ border-right: 1px solid #999;
26
+ width: 18%;
27
+ color: #222;
28
+ background-color: #eee;
29
+ }
30
+
31
+ .search, .model {
32
+ padding: 7px 15px;
33
+ }
34
+
35
+ .searchbox {
36
+ width: 100%;
37
+ font-size: 16px;
38
+ }
39
+
40
+ .model {
41
+ font-size: 20px;
42
+ cursor: pointer;
43
+ }
44
+
45
+ .model:hover {
46
+ background-color: #6495ed;
47
+ color: #eee;
48
+ }
49
+
50
+ .container {
51
+ position: fixed;
52
+ top: 0;
53
+ right: 0;
54
+ bottom: 0;
55
+ left: 18%;
56
+ }
57
+
58
+ .title {
59
+ margin-top: 30px;
60
+ text-align: center;
61
+ font-size: 32px;
62
+ }
63
+
64
+ .inner-container {
65
+ position: absolute;
66
+ background-color: white;
67
+ border: 1px solid #999;
68
+ top: 100px;
69
+ right: 30px;
70
+ bottom: 30px;
71
+ left: 30px;
72
+ }
73
+
74
+ .node {
75
+ stroke: #000111;
76
+ stroke-width: 1.0px;
77
+ fill: #6495ed;
78
+ }
79
+
80
+ .node-label {
81
+ pointer-events: none;
82
+ }
83
+
84
+ .link {
85
+ fill: none;
86
+ stroke-width: 1.5px;
87
+ stroke-opacity: 0.8;
88
+ }
89
+
90
+ .legend-outer {
91
+ width: 236px;
92
+ padding: 10px;
93
+ height: 110px;
94
+ border: 2px solid #9E9DA6;
95
+ position: fixed;
96
+ right: 44px;
97
+ bottom: 44px;
98
+ background-color: white;
99
+ }
100
+
101
+ .legend-outer legend {
102
+ font-size: large;
103
+ margin-left: auto;
104
+ margin-right: auto;
105
+ }
106
+
107
+ .ho_line {
108
+ color: #0000ff;
109
+ }
110
+
111
+ .hm_line{
112
+ color: #000000;
113
+ }
114
+
115
+ .bt_line {
116
+ color: #00ff00;
117
+ }
118
+
119
+ .habtm_line {
120
+ color: #ff0000;
121
+ }
122
+
123
+ .association_name {
124
+ margin-left: 10px;
125
+ }
126
+
127
+ .legend-has_one, .legend-has_many, .legend-belongs_to, .legend-has_and_belongs_to_many {
128
+ display: -webkit-flex;
129
+ display: flex;
130
+ cursor: pointer;
131
+ }
132
+
133
+ .d3-tip {
134
+ line-height: 1;
135
+ font-weight: bold;
136
+ padding: 12px;
137
+ background: rgba(0, 0, 0, 0.8);
138
+ color: #fff;
139
+ border-radius: 2px;
140
+ }
@@ -3,11 +3,40 @@
3
3
  <head>
4
4
  <title>Rails Model Visualizer</title>
5
5
  <meta charset="utf-8">
6
- <!-- d3 3.5.5 included from gem directory -->
6
+ <link href="<%= @css %>" rel="stylesheet" type="text/css">
7
7
  <script src="<%= @d3 %>" charset="utf-8"></script>
8
+ <script src="<%= @tooltip %>"></script>
8
9
  </head>
9
10
 
10
11
  <body>
12
+ <%= @sidebar %>
13
+
14
+ <div class="container">
15
+ <div class="title"><%= @title %></div>
16
+ <div class="inner-container">
17
+ <!-- SVG will be inserted here -->
18
+ </div>
19
+ </div>
20
+
21
+ <div class='legend-outer'>
22
+ <legend>Associations</legend>
23
+ <div class='legend-has_one' onclick="highlightAssociation('has_one')">
24
+ <div class='ho_line'>&#x25AC;</div>
25
+ <div class='association_name'>has_one</div>
26
+ </div>
27
+ <div class='legend-has_many' onclick="highlightAssociation('has_many')">
28
+ <div class='hm_line'>&#x25AC;</div>
29
+ <div class='association_name'>has_many</div>
30
+ </div>
31
+ <div class='legend-belongs_to' onclick="highlightAssociation('belongs_to')">
32
+ <div class='bt_line'>&#x25AC;</div>
33
+ <div class='association_name'>belongs_to</div>
34
+ </div>
35
+ <div class='legend-has_and_belongs_to_many' onclick="highlightAssociation('has_and_belongs_to_many')">
36
+ <div class='habtm_line'>&#x25AC;</div>
37
+ <div class='association_name'>has_and_belongs_to_many</div>
38
+ </div>
39
+ </div>
11
40
  <script>
12
41
  (function() {
13
42
  // Model data is inserted from visualizer.rb
@@ -15,6 +44,7 @@
15
44
  var nodes = [];
16
45
  var links = [];
17
46
  var node;
47
+ var nodeSelected = null;
18
48
 
19
49
  // Create node array and number each one
20
50
  var i = 0;
@@ -36,91 +66,254 @@
36
66
  node = data[name];
37
67
  for (var assn_type in node.associations) {
38
68
  node.associations[assn_type].forEach(function(assn) {
39
- links.push({
40
- source: node.node_number,
41
- target: data[assn].node_number,
42
- type: assn_type
43
- });
69
+ if (assn in data) {
70
+ links.push({
71
+ source: node.node_number,
72
+ target: data[assn].node_number,
73
+ type: assn_type
74
+ });
75
+ } else {
76
+ console.log('ERROR: association not found. ' + name + ' -> ' + assn)
77
+ }
44
78
  });
45
79
  }
46
80
  }
47
81
  }
48
82
 
49
- var width = window.innerWidth,
50
- height = window.innerHeight;
51
-
52
- var color = d3.scale.category20();
53
-
54
- var force = d3.layout
55
- .force()
56
- .charge(-450)
57
- .linkDistance(150)
58
- .size([width, height]);
83
+ var container = document.getElementsByClassName("inner-container")[0];
84
+ var width = container.clientWidth,
85
+ height = container.clientHeight;
59
86
 
60
- var svg = d3.select("body")
87
+ var svg = d3.select(".inner-container")
61
88
  .append("svg")
62
89
  .attr("width", width)
63
90
  .attr("height", height);
64
91
 
65
- force.nodes(nodes)
66
- .links(links)
67
- .start();
92
+ var force = d3.layout.force()
93
+ .size([width, height])
94
+ .nodes(d3.values(nodes))
95
+ .links(links)
96
+ .charge(-1000)
97
+ .linkDistance(200)
98
+ .start();
68
99
 
69
- var link = svg.selectAll(".link")
70
- .data(links)
71
- .enter().append("line")
100
+ var markerWidth = 8,
101
+ markerHeight = 8,
102
+ cRadius = 30, // play with the cRadius value
103
+ refX = cRadius + (markerWidth / 2),
104
+ refY = -Math.sqrt(cRadius / 3),
105
+ maxNodeLabelSize = cRadius * 0.625,
106
+ minNodeLabelSize = cRadius * 0.275,
107
+ drSub = cRadius - refY;
108
+
109
+ // build the arrow.
110
+ svg.append("svg:defs")
111
+ .selectAll("marker")
112
+ .data(["end"]) // Different link/path types can be defined here
113
+ .enter().append("svg:marker") // This section adds in the arrows
114
+ .attr("id", String)
115
+ .attr("viewBox", "0 -5 10 10")
116
+ .attr("refX", refX)
117
+ .attr("refY", refY)
118
+ .attr("markerWidth", markerWidth)
119
+ .attr("markerHeight", markerHeight)
120
+ .attr("orient", "auto")
121
+ .append("svg:path")
122
+ .attr("d", "M0,-5L10,0L0,5");
123
+
124
+ // add the links and the arrows
125
+ var path = svg.append("svg:g").selectAll("path")
126
+ .data(force.links())
127
+ .enter().append("svg:path")
72
128
  .attr("class", "link")
129
+ .attr("marker-end", "url(#end)")
73
130
  .style("stroke", function(d) {
74
131
  switch(d.type) {
75
- case "belongs_to":
76
- return "#000";
77
- case "has_and_belongs_to_many":
78
- return "#f00";
79
132
  case "has_many":
80
- return "#0f0";
133
+ return "#000000";
134
+ case "has_and_belongs_to_many":
135
+ return "#ff0000";
136
+ case "belongs_to":
137
+ return "#00ff00";
81
138
  case "has_one":
82
- return "#00f";
139
+ return "#0000ff";
83
140
  default:
84
- return "#999";
141
+ return "#999999";
85
142
  }
86
- }).style("stroke-width", function(d) { return Math.sqrt(d.value); });
143
+ });
87
144
 
88
145
  // Create the node groups under svg
89
146
  var gnodes = svg.selectAll('g.gnode')
90
147
  .data(nodes)
91
148
  .enter()
92
149
  .append('g')
150
+ .on("mouseover", mouseover)
151
+ .on("mouseout", mouseout)
152
+ .on("click", click)
93
153
  .classed('gnode', true);
94
154
 
95
155
  // Add circle to each group
96
156
  node = gnodes.append("circle")
97
157
  .attr("class", "node")
98
- .attr("r", 15)
99
- .style("fill", function(d) { return color(d.group); })
158
+ .attr("r", cRadius)
100
159
  .call(force.drag);
101
160
 
102
161
  // Append labels to each group
103
162
  var labels = gnodes.append("text")
104
- .text(function(d) { return d.name; });
105
-
106
- // Show model attributes on hover
107
- node.append("title").text(function(d) {
108
- var description = d.name + '\r\n';
163
+ .text(function(d) { return d.name; })
164
+ .attr("text-anchor", "middle")
165
+ .attr("class", "node-label")
166
+ .style("font-size", "1px")
167
+ .each(getSize)
168
+ .style("font-size", function(d) { return d.scale + "px"; })
169
+ .attr("dy", function(d) { return (d.scale / 5) + "px"; });
109
170
 
171
+ // initializes the tooltip for node mouseover
172
+ var tip = d3.tip()
173
+ .attr('class', 'd3-tip')
174
+ .offset([-10, 0])
175
+ .html(function(d) {
176
+ var description = d.name + '<br>';
110
177
  for (var attr in d.schema_info) {
111
- if (d.schema_info[attr].length > 0) {
112
- description += '\r\n' + attr + ": " + d.schema_info[attr].join(', ');
113
- }
178
+ if (d.schema_info[attr].length > 0) {
179
+ description += '<br>' + attr + ": " + d.schema_info[attr].join(', ');
180
+ }
114
181
  }
115
-
116
182
  return description;
117
- });
183
+ })
184
+
185
+ // svg calls tip for tooltip functionality
186
+ svg.call(tip);
187
+
188
+ function getSize(d) {
189
+ var bbox = this.getBBox(),
190
+ margin = 3.0,
191
+ cbbox = this.parentNode.getBBox(),
192
+ scale = Math.min(cbbox.width/bbox.width, cbbox.height/bbox.height);
193
+ scale = Math.min(scale, maxNodeLabelSize)
194
+ scale = Math.max(scale, minNodeLabelSize)
195
+ d.scale = (scale - margin);
196
+ }
197
+
198
+ function mouseover() {
199
+ if (nodeSelected === null) {
200
+ d3.select(this).select("circle").transition()
201
+ .duration(500)
202
+ .attr("r", function(d){
203
+ return cRadius * 1.1;
204
+ })
205
+ .style("fill", function(d) {
206
+ return "#90EE90";
207
+ });
208
+ d3.select(this).select('text').transition()
209
+ .duration(500)
210
+ .style("font-size", function(d) { return d.scale * 1.1; });
211
+ // A little hackish, but couldn't figure out how to use mouseover with anonymous function inside
212
+ tip.show(d3.select(this)[0][0].__data__);
213
+ }
214
+ }
215
+
216
+ function mouseout() {
217
+ if (nodeSelected === null) {
218
+ d3.select(this).select('text').transition()
219
+ .duration(500)
220
+ .style("font-size", function(d) { return d.scale; });
221
+ d3.select(this).select("circle").transition()
222
+ .duration(500)
223
+ .attr("r", function(d){
224
+ return cRadius;
225
+ })
226
+ .style("fill", function(d) {
227
+ return "#6495ED";
228
+ });
229
+ // same comment as mouseover ^^^
230
+ tip.hide(d3.select(this)[0][0].__data__);
231
+ }
232
+ }
233
+
234
+ function click() {
235
+ // if we are clicking a node and there is no node currently selected
236
+ if (nodeSelected === null) {
237
+ nodeSelected = d3.select(this);
238
+ nodeSelected.select("circle").transition()
239
+ .duration(500)
240
+ .attr("r", function(d){
241
+ return cRadius * 1.1;
242
+ })
243
+ .style("fill", function(d) {
244
+ return "#90EE90";
245
+ });
246
+ nodeSelected.select('text').transition()
247
+ .duration(500)
248
+ .style("font-size", function(d) { return d.scale * 1.1; });
249
+ tip.show(nodeSelected[0][0].__data__);
250
+ }
251
+ // if we are clicking a node that is currently selected
252
+ else if(nodeSelected[0][0].__data__.index === d3.select(this)[0][0].__data__.index) {
253
+ nodeSelected.select('text').transition()
254
+ .duration(500)
255
+ .style("font-size", function(d) { return d.scale; });
256
+ nodeSelected.select("circle").transition()
257
+ .duration(500)
258
+ .attr("r", function(d){
259
+ return cRadius;
260
+ })
261
+ .style("fill", function(d) {
262
+ return "#6495ED";
263
+ });
264
+ tip.hide(nodeSelected[0][0].__data__);
265
+
266
+ nodeSelected = null;
267
+ }
268
+ // if we are clicking a node that is not already selected
269
+ else {
270
+ nodeSelected.select('text').transition()
271
+ .duration(500)
272
+ .style("font-size", function(d) { return d.scale; });
273
+ nodeSelected.select("circle").transition()
274
+ .duration(500)
275
+ .attr("r", function(d){
276
+ return cRadius;
277
+ })
278
+ .style("fill", function(d) {
279
+ return "#6495ED";
280
+ });
281
+ tip.hide(nodeSelected[0][0].__data__);
282
+
283
+ nodeSelected = d3.select(this);
284
+
285
+ nodeSelected.select("circle").transition()
286
+ .duration(500)
287
+ .attr("r", function(d){
288
+ return cRadius * 1.1;
289
+ })
290
+ .style("fill", function(d) {
291
+ return "#90EE90";
292
+ });
293
+ nodeSelected.select('text').transition()
294
+ .duration(500)
295
+ .style("font-size", function(d) { return d.scale * 1.1; });
296
+ tip.show(d3.select(this)[0][0].__data__);
297
+ }
298
+ }
118
299
 
119
300
  force.on("tick", function() {
120
- link.attr("x1", function(d) { return d.source.x; })
121
- .attr("y1", function(d) { return d.source.y; })
122
- .attr("x2", function(d) { return d.target.x; })
123
- .attr("y2", function(d) { return d.target.y; });
301
+ path.attr("d", function(d) {
302
+ var dx = d.target.x - d.source.x,
303
+ dy = d.target.y - d.source.y,
304
+ dr = Math.sqrt(dx * dx + dy * dy);
305
+ return "M" +
306
+ d.source.x + "," +
307
+ d.source.y + "A" +
308
+ (dr - drSub) + "," + (dr - drSub) + " 0 0,1 " +
309
+ d.target.x + "," +
310
+ d.target.y;
311
+ });
312
+
313
+ // link.attr("x1", function(d) { return d.source.x; })
314
+ // .attr("y1", function(d) { return d.source.y; })
315
+ // .attr("x2", function(d) { return d.target.x; })
316
+ // .attr("y2", function(d) { return d.target.y; });
124
317
 
125
318
  // Translate the groups
126
319
  gnodes.attr("transform", function(d) {
@@ -129,7 +322,73 @@
129
322
  // node.attr("cx", function(d) { return d.x; })
130
323
  // .attr("cy", function(d) { return d.y; });
131
324
  });
325
+
326
+ // Search functionality for sidebar
327
+ var searchdiv = document.getElementsByClassName('sidebar')[0].firstChild;
328
+ var searchfield = searchdiv.firstChild;
329
+ searchfield.oninput = function() {
330
+ var sidebarentries = document.getElementsByClassName('model');
331
+ var search = searchfield.value.toLowerCase();
332
+ for (var i = 0; i < sidebarentries.length; i++) {
333
+ var entry = sidebarentries[i];
334
+ var html = entry.innerHTML.toLowerCase();
335
+ if (html.indexOf(search) == -1) {
336
+ entry.style.display = 'none';
337
+ } else {
338
+ entry.style.display = 'block';
339
+ }
340
+ }
341
+ }
342
+
343
+ // Highlight node on click. Must be global.
344
+ var nodeHighlightTimeout;
345
+ window.highlightNode = function(name) {
346
+ if (nodeHighlightTimeout !== null) {
347
+ clearTimeout(nodeHighlightTimeout);
348
+ }
349
+
350
+ // Dim all links and all nodes but the one with the given name
351
+ var gnodes = svg.selectAll(".gnode");
352
+
353
+ gnodes.filter(function (d, i) {
354
+ return d.name != name;
355
+ }).style("opacity", "0.1");
356
+
357
+ gnodes.filter(function (d, i) {
358
+ return d.name == name;
359
+ }).style("opacity", "1.0");
360
+
361
+ svg.selectAll(".link").style("opacity", "0.1");
362
+
363
+ // TODO: display tooltip and keep everything else dimmed to undim later
364
+ nodeHighlightTimeout = setTimeout(function() {
365
+ d3.selectAll(".gnode, .link")
366
+ .style("opacity", "1");
367
+ }, 1500);
368
+ }
369
+
370
+ window.highlightAssociation = function(association) {
371
+ //dim all OTHER types of associations
372
+ svg.selectAll(".link").filter(function (d, i) {
373
+ return d.type != association;
374
+ }).style("opacity", "0.1");
375
+
376
+ // svg.selectAll(".link").filter(function (d, i) {
377
+ // return d.type = association;
378
+ // }).style("stroke-width", "3.5px");
379
+
380
+ d3.selectAll(".gnode, .link").transition()
381
+ .duration(4500)
382
+ .style("opacity", "1");
383
+
384
+ // d3.selectAll(".link").transition()
385
+ // .duration(1500)
386
+ // .style("stroke-width", "1.5px");
387
+
388
+ }
389
+
132
390
  })();
133
391
  </script>
392
+
134
393
  </body>
135
394
  </html>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: model-visualizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Grover
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2015-04-09 00:00:00.000000000 Z
14
+ date: 2015-05-28 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -41,7 +41,11 @@ files:
41
41
  - lib/model-visualizer/model-parser.rb
42
42
  - lib/model-visualizer/schema-parser.rb
43
43
  - share/template.html
44
+ - share/main.css
45
+ - share/background.png
46
+ - share/OpenSans-Regular.ttf
44
47
  - share/d3.min.js
48
+ - share/d3.tip.js
45
49
  - bin/model-visualizer
46
50
  homepage: http://rubygems.org/gems/model-visualizer
47
51
  licenses: