wads 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ced90f1073417f101cc5688139f63c6437814a7bf96249b54344ad3a72f5a0fc
4
- data.tar.gz: '08fafccb767210d6f3653ee9ab6c33ca01962b4bd71503c54a3afcca83efcd9f'
3
+ metadata.gz: dffb479b6d7bdc47ff103db3f88b1acece5d0ec672361070aec604eb25aa6e9b
4
+ data.tar.gz: b9fd53c1345bc7f5ab18cd0b5ab2eb74cc0b3276cd538cdae6dad414523f9529
5
5
  SHA512:
6
- metadata.gz: 2daa8f10e3cabab4c8824f5f070b5b9f5b124b5c90910b6bd9d0a75c8775e8ed1285ed4cb2a019a08320f27c735065789a2401e60b2098731d6e7d915073868d
7
- data.tar.gz: 235bd6b70e43ef6f8714ea6406598a49747f5846495193d5193d089acf09b30ef80ff5dbf91733c6eb6e0e08468f34b192735262900b31b89d4caf71b41a1648
6
+ metadata.gz: dbeb4a1dd4f5918d047954b6336219e3dd9429db3de755f56f12f34cf60c4e187d92a967a3cbde88ad02dc35952257da492729f1da762f8b49035775462b4b1b
7
+ data.tar.gz: 6c28345cfc8658cd25c1243469ff0fa308aa8278597b48e42f9cdd71b1a4d7e285e3df989d19504ea2f3137a36e593ee6a46116a360d4ed92d627c2b9b334619
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
- ## [Unreleased]
2
-
3
1
  ## [0.1.0] - 2021-08-23
4
2
 
5
3
  - Initial release
4
+
5
+ ## [0.1.1] - 2021-08-29
6
+
7
+ - Added graph data structure and corresponding display widgets
8
+
data/README.md CHANGED
@@ -25,3 +25,15 @@ cd ruby-wads
25
25
 
26
26
  This will run the sample NASDAQ stocks analysis that features the use of the
27
27
  Stats class, and a table Gosu widget used to display the results of the analysis.
28
+
29
+ You can also see the graph capabilities by running the Star Wars analysis example.
30
+ This uses a data set that captures all character interactions within Episode 4.
31
+
32
+ ```
33
+ ./run-sample-app -j -g
34
+ ```
35
+
36
+ ## References
37
+
38
+ The Star Wars data set is courtesy of:
39
+ Gabasova, E. (2016). Star Wars social network. DOI: https://doi.org/10.5281/zenodo.1411479.
@@ -0,0 +1,336 @@
1
+ {
2
+ "nodes": [
3
+ {
4
+ "name": "C-3PO",
5
+ "value": 42,
6
+ "colour": "#FFD700"
7
+ },
8
+ {
9
+ "name": "LUKE",
10
+ "value": 88,
11
+ "colour": "#3881e5"
12
+ },
13
+ {
14
+ "name": "DARTH VADER",
15
+ "value": 25,
16
+ "colour": "#000000"
17
+ },
18
+ {
19
+ "name": "CAMIE",
20
+ "value": 3,
21
+ "colour": "#808080"
22
+ },
23
+ {
24
+ "name": "BIGGS",
25
+ "value": 18,
26
+ "colour": "#808080"
27
+ },
28
+ {
29
+ "name": "LEIA",
30
+ "value": 25,
31
+ "colour": "#DCDCDC"
32
+ },
33
+ {
34
+ "name": "BERU",
35
+ "value": 5,
36
+ "colour": "#808080"
37
+ },
38
+ {
39
+ "name": "OWEN",
40
+ "value": 6,
41
+ "colour": "#808080"
42
+ },
43
+ {
44
+ "name": "OBI-WAN",
45
+ "value": 22,
46
+ "colour": "#48D1CC"
47
+ },
48
+ {
49
+ "name": "MOTTI",
50
+ "value": 3,
51
+ "colour": "#808080"
52
+ },
53
+ {
54
+ "name": "TARKIN",
55
+ "value": 12,
56
+ "colour": "#808080"
57
+ },
58
+ {
59
+ "name": "HAN",
60
+ "value": 44,
61
+ "colour": "#ff9400"
62
+ },
63
+ {
64
+ "name": "GREEDO",
65
+ "value": 2,
66
+ "colour": "#808080"
67
+ },
68
+ {
69
+ "name": "JABBA",
70
+ "value": 2,
71
+ "colour": "#808080"
72
+ },
73
+ {
74
+ "name": "DODONNA",
75
+ "value": 3,
76
+ "colour": "#808080"
77
+ },
78
+ {
79
+ "name": "GOLD LEADER",
80
+ "value": 14,
81
+ "colour": "#808080"
82
+ },
83
+ {
84
+ "name": "WEDGE",
85
+ "value": 15,
86
+ "colour": "#808080"
87
+ },
88
+ {
89
+ "name": "RED LEADER",
90
+ "value": 33,
91
+ "colour": "#808080"
92
+ },
93
+ {
94
+ "name": "RED TEN",
95
+ "value": 8,
96
+ "colour": "#808080"
97
+ },
98
+ {
99
+ "name": "GOLD FIVE",
100
+ "value": 8,
101
+ "colour": "#808080"
102
+ }
103
+ ],
104
+ "links": [
105
+ {
106
+ "source": 3,
107
+ "target": 1,
108
+ "value": 2
109
+ },
110
+ {
111
+ "source": 4,
112
+ "target": 3,
113
+ "value": 2
114
+ },
115
+ {
116
+ "source": 4,
117
+ "target": 1,
118
+ "value": 4
119
+ },
120
+ {
121
+ "source": 2,
122
+ "target": 5,
123
+ "value": 1
124
+ },
125
+ {
126
+ "source": 6,
127
+ "target": 1,
128
+ "value": 3
129
+ },
130
+ {
131
+ "source": 6,
132
+ "target": 7,
133
+ "value": 3
134
+ },
135
+ {
136
+ "source": 6,
137
+ "target": 0,
138
+ "value": 2
139
+ },
140
+ {
141
+ "source": 1,
142
+ "target": 7,
143
+ "value": 3
144
+ },
145
+ {
146
+ "source": 0,
147
+ "target": 1,
148
+ "value": 18
149
+ },
150
+ {
151
+ "source": 0,
152
+ "target": 7,
153
+ "value": 2
154
+ },
155
+ {
156
+ "source": 0,
157
+ "target": 5,
158
+ "value": 6
159
+ },
160
+ {
161
+ "source": 5,
162
+ "target": 1,
163
+ "value": 17
164
+ },
165
+ {
166
+ "source": 6,
167
+ "target": 5,
168
+ "value": 1
169
+ },
170
+ {
171
+ "source": 1,
172
+ "target": 8,
173
+ "value": 19
174
+ },
175
+ {
176
+ "source": 0,
177
+ "target": 8,
178
+ "value": 6
179
+ },
180
+ {
181
+ "source": 5,
182
+ "target": 8,
183
+ "value": 1
184
+ },
185
+ {
186
+ "source": 9,
187
+ "target": 10,
188
+ "value": 2
189
+ },
190
+ {
191
+ "source": 2,
192
+ "target": 9,
193
+ "value": 1
194
+ },
195
+ {
196
+ "source": 2,
197
+ "target": 10,
198
+ "value": 7
199
+ },
200
+ {
201
+ "source": 11,
202
+ "target": 8,
203
+ "value": 9
204
+ },
205
+ {
206
+ "source": 11,
207
+ "target": 1,
208
+ "value": 26
209
+ },
210
+ {
211
+ "source": 12,
212
+ "target": 11,
213
+ "value": 1
214
+ },
215
+ {
216
+ "source": 11,
217
+ "target": 13,
218
+ "value": 1
219
+ },
220
+ {
221
+ "source": 0,
222
+ "target": 11,
223
+ "value": 6
224
+ },
225
+ {
226
+ "source": 5,
227
+ "target": 9,
228
+ "value": 1
229
+ },
230
+ {
231
+ "source": 5,
232
+ "target": 10,
233
+ "value": 1
234
+ },
235
+ {
236
+ "source": 11,
237
+ "target": 5,
238
+ "value": 13
239
+ },
240
+ {
241
+ "source": 2,
242
+ "target": 8,
243
+ "value": 1
244
+ },
245
+ {
246
+ "source": 14,
247
+ "target": 15,
248
+ "value": 1
249
+ },
250
+ {
251
+ "source": 14,
252
+ "target": 16,
253
+ "value": 1
254
+ },
255
+ {
256
+ "source": 14,
257
+ "target": 1,
258
+ "value": 1
259
+ },
260
+ {
261
+ "source": 15,
262
+ "target": 16,
263
+ "value": 1
264
+ },
265
+ {
266
+ "source": 15,
267
+ "target": 1,
268
+ "value": 1
269
+ },
270
+ {
271
+ "source": 1,
272
+ "target": 16,
273
+ "value": 2
274
+ },
275
+ {
276
+ "source": 4,
277
+ "target": 5,
278
+ "value": 1
279
+ },
280
+ {
281
+ "source": 5,
282
+ "target": 17,
283
+ "value": 1
284
+ },
285
+ {
286
+ "source": 1,
287
+ "target": 17,
288
+ "value": 3
289
+ },
290
+ {
291
+ "source": 4,
292
+ "target": 17,
293
+ "value": 3
294
+ },
295
+ {
296
+ "source": 4,
297
+ "target": 0,
298
+ "value": 1
299
+ },
300
+ {
301
+ "source": 0,
302
+ "target": 17,
303
+ "value": 1
304
+ },
305
+ {
306
+ "source": 17,
307
+ "target": 16,
308
+ "value": 3
309
+ },
310
+ {
311
+ "source": 15,
312
+ "target": 17,
313
+ "value": 1
314
+ },
315
+ {
316
+ "source": 4,
317
+ "target": 16,
318
+ "value": 2
319
+ },
320
+ {
321
+ "source": 17,
322
+ "target": 18,
323
+ "value": 1
324
+ },
325
+ {
326
+ "source": 4,
327
+ "target": 15,
328
+ "value": 1
329
+ },
330
+ {
331
+ "source": 1,
332
+ "target": 18,
333
+ "value": 1
334
+ }
335
+ ]
336
+ }
data/lib/wads/app.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'gosu'
2
+ require 'json'
2
3
  require_relative 'data_structures'
3
4
  require_relative 'widgets'
4
5
  require_relative 'version'
@@ -9,6 +10,7 @@ class WadsSampleApp < Gosu::Window
9
10
 
10
11
  STOCKS_DATA_FILE = "./data/NASDAQ.csv"
11
12
  LOTTERY_DATA_FILE = "./data/Pick4_12_21_2020.txt"
13
+ STAR_WARS_DATA_FILE = "./data/starwars-episode-4-interactions.json"
12
14
 
13
15
  def initialize
14
16
  super(800, 600)
@@ -19,6 +21,7 @@ class WadsSampleApp < Gosu::Window
19
21
  @small_font = Gosu::Font.new(22)
20
22
  @banner_image = Gosu::Image.new("./media/Banner.png")
21
23
  @display_widget = nil
24
+ @update_count = 0
22
25
  end
23
26
 
24
27
  def parse_opts_and_run
@@ -36,13 +39,18 @@ class WadsSampleApp < Gosu::Window
36
39
  else
37
40
  stats.report(Date::DAYNAMES[1..5])
38
41
  end
39
-
42
+
43
+ elsif opts[:jedi]
44
+ graph = process_star_wars_data
45
+ @display_widget = SampleStarWarsDisplay.new(@small_font, graph)
46
+ show
40
47
  elsif opts[:lottery]
41
48
  process_lottery_data
42
49
  else
43
50
  puts " "
44
51
  puts "Select one of the following sample analysis options"
45
52
  puts "-s Run sample stocks analysis"
53
+ puts "-j Run sample Star Wars character analysis"
46
54
  puts "-l Run sample analysis of lottery numbers"
47
55
  puts " "
48
56
  exit
@@ -51,7 +59,8 @@ class WadsSampleApp < Gosu::Window
51
59
  end
52
60
 
53
61
  def update
54
- # TODO
62
+ @display_widget.update(@update_count, mouse_x, mouse_y)
63
+ @update_count = @update_count + 1
55
64
  end
56
65
 
57
66
  def draw
@@ -74,6 +83,10 @@ class WadsSampleApp < Gosu::Window
74
83
  end
75
84
  end
76
85
 
86
+ def button_up id
87
+ @display_widget.button_up id, mouse_x, mouse_y
88
+ end
89
+
77
90
  def process_stock_data
78
91
  # The data file comes from https://finance.yahoo.com
79
92
  # The format of this file is as follows:
@@ -130,6 +143,41 @@ class WadsSampleApp < Gosu::Window
130
143
  puts "and the same pattern used in the stocks example can"
131
144
  puts "be applied here."
132
145
  end
146
+
147
+ def process_star_wars_data
148
+ star_wars_json = File.read(STAR_WARS_DATA_FILE)
149
+ data_hash = JSON.parse(star_wars_json)
150
+ characters = data_hash['nodes']
151
+ interactions = data_hash['links']
152
+
153
+ # The interactions in the data set reference the characters by their
154
+ # zero based index, so we keep a reference in our graph by index.
155
+ # The character's value is the number of scenes in which they appear.
156
+ graph = Graph.new
157
+ characters.each do |character|
158
+ node_tags = {}
159
+ node_color_str = character['colour']
160
+ # This is a bit of a hack, but our background is black so black text
161
+ # will not show up. Change this to white
162
+ if node_color_str == "#000000"
163
+ node_color_str = "#FFFFFF"
164
+ end
165
+ # Convert hex string (ex. "#EE00AA") into int hex representation
166
+ # understood by Gosu color (ex. 0xFFEE00AA)
167
+ node_color = "0xFF#{node_color_str[1..-1]}".to_i(16)
168
+ node_tags['color'] = node_color
169
+ graph.add_node(Node.new(character['name'], character['value'], node_tags))
170
+ end
171
+ interactions.each do |interaction|
172
+ character_one = graph.node_by_index(interaction['source'])
173
+ character_two = graph.node_by_index(interaction['target'])
174
+ number_of_scenes_together = interaction['value']
175
+ edge_tags = {}
176
+ edge_tags["scenes"] = number_of_scenes_together
177
+ character_one.add_output_edge(character_two, edge_tags)
178
+ end
179
+ graph
180
+ end
133
181
  end
134
182
 
135
183
  class SampleStocksDisplay < Widget
@@ -196,4 +244,72 @@ class SampleStocksDisplay < Widget
196
244
  end
197
245
  WidgetResult.new(false)
198
246
  end
247
+ end
248
+
249
+ class SampleStarWarsDisplay < Widget
250
+ attr_accessor :graph
251
+
252
+ def initialize(font, graph)
253
+ super(10, 100, COLOR_HEADER_BRIGHT_BLUE)
254
+ set_dimensions(780, 500)
255
+ set_font(font)
256
+ add_child(Document.new(sample_content, x + 5, y + 5, @width, @height, @font))
257
+ @exit_button = Button.new("Exit", 380, bottom_edge - 30, @font)
258
+ add_child(@exit_button)
259
+
260
+ @graph = graph
261
+ @data_table = SingleSelectTable.new(@x + 5, @y + 70, # top left corner
262
+ 770, 180, # width, height
263
+ ["Character", "Number of Scenes"], # column headers
264
+ @font, COLOR_WHITE, # font and text color
265
+ 5) # max visible rows
266
+ @data_table.selected_color = COLOR_LIGHT_GRAY
267
+ @graph.node_list.each do |character|
268
+ @data_table.add_row([character.name, character.value], character.get_tag("color"))
269
+ end
270
+ add_child(@data_table)
271
+ @graph_display = GraphWidget.new(x + 5, y + 260, 770, 200, @font, @color, @graph)
272
+ end
273
+
274
+ def sample_content
275
+ <<~HEREDOC
276
+ This sample analysis shows the interactions between characters in the Star Wars
277
+ Episode 4: A New Hope. Click on a character to see more detail.
278
+ HEREDOC
279
+ end
280
+
281
+ def render
282
+ @graph_display.draw
283
+ end
284
+
285
+ def button_down id, mouse_x, mouse_y
286
+ if id == Gosu::MsLeft
287
+ if @exit_button.contains_click(mouse_x, mouse_y)
288
+ return WidgetResult.new(true)
289
+ elsif @data_table.contains_click(mouse_x, mouse_y)
290
+ val = @data_table.set_selected_row(mouse_y, 0)
291
+ if val.nil?
292
+ # nothing to do
293
+ else
294
+ node = @graph.find_node(val)
295
+ @graph_display.set_display(node, 2)
296
+ end
297
+ elsif @graph_display.contains_click(mouse_x, mouse_y)
298
+ @graph_display.button_down id, mouse_x, mouse_y
299
+ end
300
+ elsif id == Gosu::KbUp
301
+ @data_table.scroll_up
302
+ elsif id == Gosu::KbDown
303
+ @data_table.scroll_down
304
+ end
305
+ WidgetResult.new(false)
306
+ end
307
+
308
+ def button_up id, mouse_x, mouse_y
309
+ @graph_display.button_up id, mouse_x, mouse_y
310
+ end
311
+
312
+ def update update_count, mouse_x, mouse_y
313
+ @graph_display.update update_count, mouse_x, mouse_y
314
+ end
199
315
  end
@@ -4,7 +4,25 @@ module Wads
4
4
 
5
5
  SPACER = " "
6
6
  VALUE_WIDTH = 10
7
- NODE_UNKNOWN = "undefined"
7
+
8
+ DEG_0 = 0
9
+ DEG_45 = Math::PI * 0.25
10
+ DEG_90 = Math::PI * 0.5
11
+ DEG_135 = Math::PI * 0.75
12
+ DEG_180 = Math::PI
13
+ DEG_225 = Math::PI * 1.25
14
+ DEG_270 = Math::PI * 1.5
15
+ DEG_315 = Math::PI * 1.75
16
+ DEG_360 = Math::PI * 2
17
+
18
+ DEG_22_5 = Math::PI * 0.125
19
+ DEG_67_5 = DEG_45 + DEG_22_5
20
+ DEG_112_5 = DEG_90 + DEG_22_5
21
+ DEG_157_5 = DEG_135 + DEG_22_5
22
+ DEG_202_5 = DEG_180 + DEG_22_5
23
+ DEG_247_5 = DEG_225 + DEG_22_5
24
+ DEG_292_5 = DEG_270 + DEG_22_5
25
+ DEG_337_5 = DEG_315 + DEG_22_5
8
26
 
9
27
  class HashOfHashes
10
28
  attr_accessor :data
@@ -225,27 +243,198 @@ module Wads
225
243
  end
226
244
 
227
245
  class Node
228
- attr_accessor :x
229
- attr_accessor :y
230
246
  attr_accessor :name
231
- attr_accessor :type
232
- attr_accessor :inputs
247
+ attr_accessor :value
248
+ attr_accessor :backlinks
233
249
  attr_accessor :outputs
234
250
  attr_accessor :visited
251
+ attr_accessor :tags
235
252
 
236
- def initialize(name, type = NODE_UNKNOWN)
237
- @name = name
238
- @type = type
239
- @inputs = []
253
+ def initialize(name, value = nil, tags = {})
254
+ @name = name
255
+ @value = value
256
+ @backlinks = []
240
257
  @outputs = []
241
258
  @visited = false
259
+ @tags = tags
260
+ end
261
+
262
+ def add_child(name, value)
263
+ add_output(name, value)
242
264
  end
243
265
 
244
- #
245
- # TODO Visitor pattern and solution for detecting cyclic graphs
246
- #
247
- # when you visit, reset all the visited flags
248
- # set it to true when you visit the node
249
- # first check though if visited already true, if so, you have a cycle
266
+ def add_output(name, value)
267
+ child_node = Node.new(name, value)
268
+ add_output_node(child_node)
269
+ end
270
+
271
+ def add_output_node(child_node)
272
+ child_node.backlinks << self
273
+ @outputs << child_node
274
+ child_node
275
+ end
276
+
277
+ def add_output_edge(destination, tags = {})
278
+ edge = Edge.new(destination, tags)
279
+ destination.backlinks << self
280
+ @outputs << edge
281
+ edge
282
+ end
283
+
284
+ def add_tag(key, value)
285
+ @tags[key] = value
286
+ end
287
+
288
+ def get_tag(key)
289
+ @tags[key]
290
+ end
291
+
292
+ def find_node(search_name)
293
+ if @name == search_name
294
+ return self
295
+ end
296
+ found_node_in_child = nil
297
+
298
+ @outputs.each do |child|
299
+ if child.is_a? Edge
300
+ child = child.destination
301
+ end
302
+ found_node_in_child = child.find_node(search_name)
303
+ if found_node_in_child
304
+ return found_node_in_child
305
+ end
306
+ end
307
+ nil
308
+ end
309
+
310
+ def visit(&block)
311
+ node_queue = [self]
312
+ until node_queue.empty?
313
+ node = node_queue.shift
314
+ yield node
315
+ node.outputs.each do |c|
316
+ if child.is_a? Edge
317
+ child = child.destination
318
+ end
319
+ node_queue << c
320
+ end
321
+ end
322
+ end
323
+
324
+ def to_display
325
+ "#{@name}: #{value} inputs: #{@backlinks.size} outputs: #{@outputs.size}"
326
+ end
327
+
328
+ def full_display
329
+ puts to_display
330
+ @backlinks.each do |i|
331
+ puts "Input: #{i.name}"
332
+ end
333
+ #@outputs.each do |o|
334
+ # puts "Output: #{o.name}"
335
+ #end
336
+ end
337
+ end
338
+
339
+ class Edge
340
+ attr_accessor :destination
341
+ attr_accessor :tags
342
+
343
+ def initialize(destination, tags = {})
344
+ @destination = destination
345
+ @tags = tags
346
+ end
347
+
348
+ def add_tag(key, value)
349
+ @tags[key] = value
350
+ end
351
+
352
+ def get_tag(key)
353
+ @tags[key]
354
+ end
355
+ end
356
+
357
+ class Graph
358
+ attr_accessor :node_list
359
+ attr_accessor :node_map
360
+
361
+ def initialize
362
+ @node_list = []
363
+ @node_map = {}
364
+ end
365
+
366
+ def add_node(node)
367
+ @node_list << node
368
+ @node_map[node.name] = node
369
+ end
370
+
371
+ def add_edge(source, target, tags)
372
+ if source.is_a? String
373
+ source = find_node(source)
374
+ end
375
+ if target.is_a? String
376
+ target = find_node(target)
377
+ end
378
+ end
379
+
380
+ def find_node(name)
381
+ @node_map[name]
382
+ end
383
+
384
+ def node_by_index(index)
385
+ @node_list[index]
386
+ end
387
+
388
+ def reset_visited
389
+ @node_list.each do |node|
390
+ node.visited = false
391
+ end
392
+ end
393
+
394
+ def root_nodes
395
+ list = []
396
+ @node_list.each do |node|
397
+ if node.backlinks.empty?
398
+ list << node
399
+ end
400
+ end
401
+ list
402
+ end
403
+
404
+ def is_cycle(node)
405
+ reset_visited
406
+ node.visit do |n|
407
+ if n.visited
408
+ return true
409
+ else
410
+ n.visited = true
411
+ end
412
+ end
413
+ false
414
+ end
415
+
416
+ def fan_out(node, max_depth, current_depth = 1)
417
+ if current_depth > max_depth
418
+ return {}
419
+ end
420
+ map = {}
421
+ map[node.name] = node
422
+ node.backlinks.each do |child|
423
+ map_from_child = fan_out(child, max_depth, current_depth + 1)
424
+ map_from_child.each do |key, value|
425
+ map[key] = value
426
+ end
427
+ end
428
+ node.outputs.each do |child|
429
+ if child.is_a? Edge
430
+ child = child.destination
431
+ end
432
+ map_from_child = fan_out(child, max_depth, current_depth + 1)
433
+ map_from_child.each do |key, value|
434
+ map[key] = value
435
+ end
436
+ end
437
+ map
438
+ end
250
439
  end
251
440
  end
data/lib/wads/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Wads
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
data/lib/wads/widgets.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require_relative 'data_structures'
2
+
1
3
  module Wads
2
4
  COLOR_PEACH = Gosu::Color.argb(0xffe6b0aa)
3
5
  COLOR_LIGHT_PURPLE = Gosu::Color.argb(0xffd7bde2)
@@ -28,8 +30,8 @@ module Wads
28
30
 
29
31
  Z_ORDER_BACKGROUND = 2
30
32
  Z_ORDER_WIDGET_BORDER = 3
31
- Z_ORDER_GRAPHIC_ELEMENTS = 4
32
- Z_ORDER_SELECTION_BACKGROUND = 5
33
+ Z_ORDER_SELECTION_BACKGROUND = 4
34
+ Z_ORDER_GRAPHIC_ELEMENTS = 5
33
35
  Z_ORDER_PLOT_POINTS = 6
34
36
  Z_ORDER_OVERLAY_BACKGROUND = 7
35
37
  Z_ORDER_OVERLAY_ELEMENTS = 8
@@ -94,6 +96,10 @@ module Wads
94
96
  @x + ((right_edge - @x) / 2)
95
97
  end
96
98
 
99
+ def center_y
100
+ @y + ((bottom_edge - @y) / 2)
101
+ end
102
+
97
103
  def draw
98
104
  if @visible
99
105
  render
@@ -109,8 +115,13 @@ module Wads
109
115
  end
110
116
  end
111
117
 
112
- def draw_background
113
- Gosu::draw_rect(@x + 1, @y + 1, @width - 1, @height - 1, @background_color, Z_ORDER_BACKGROUND)
118
+ def draw_background(z_order = Z_ORDER_BACKGROUND)
119
+ Gosu::draw_rect(@x + 1, @y + 1, @width - 3, @height - 3, @background_color, z_order)
120
+ end
121
+
122
+ def draw_shadow(color, z_order = Z_ORDER_WIDGET_BORDER)
123
+ Gosu::draw_line @x - 1, @y - 1, color, right_edge - 1, @y - 1, color, z_order
124
+ Gosu::draw_line @x - 1, @y - 1, color, @x - 1, bottom_edge - 1, color, z_order
114
125
  end
115
126
 
116
127
  def render
@@ -119,19 +130,35 @@ module Wads
119
130
  # render is for specific drawing done by the widget
120
131
  end
121
132
 
122
- def draw_border(color = nil)
133
+ def draw_border(color = nil, zorder = Z_ORDER_WIDGET_BORDER)
123
134
  if color.nil?
124
- color = @color
135
+ if @border_color
136
+ color = @border_color
137
+ else
138
+ color = @color
139
+ end
125
140
  end
126
- Gosu::draw_line @x, @y, color, right_edge, @y, color, Z_ORDER_WIDGET_BORDER
127
- Gosu::draw_line @x, @y, color, @x, bottom_edge, color, Z_ORDER_WIDGET_BORDER
128
- Gosu::draw_line @x,bottom_edge, color, right_edge, bottom_edge, color, Z_ORDER_WIDGET_BORDER
129
- Gosu::draw_line right_edge, @y, color, right_edge, bottom_edge, color, Z_ORDER_WIDGET_BORDER
141
+ Gosu::draw_line @x, @y, color, right_edge, @y, color, zorder
142
+ Gosu::draw_line @x, @y, color, @x, bottom_edge, color, zorder
143
+ Gosu::draw_line @x,bottom_edge, color, right_edge, bottom_edge, color, zorder
144
+ Gosu::draw_line right_edge, @y, color, right_edge, bottom_edge, color, zorder
130
145
  end
131
146
 
132
147
  def contains_click(mouse_x, mouse_y)
133
148
  mouse_x >= @x and mouse_x <= right_edge and mouse_y >= @y and mouse_y <= bottom_edge
134
149
  end
150
+
151
+ def update update_count, mouse_x, mouse_y
152
+ # empty base implementation
153
+ end
154
+
155
+ def button_down id, mouse_x, mouse_y
156
+ # empty base implementation
157
+ end
158
+
159
+ def button_up id, mouse_x, mouse_y
160
+ # empty base implementation
161
+ end
135
162
  end
136
163
 
137
164
  class Text < Widget
@@ -204,7 +231,7 @@ module Wads
204
231
  end
205
232
 
206
233
  def render
207
- draw_border(COLOR_WHITE)
234
+ draw_border(@color)
208
235
  text_x = center_x - (@text_pixel_width / 2)
209
236
  @font.draw_text(@label, text_x, @y, Z_ORDER_TEXT, 1, 1, @text_color)
210
237
  end
@@ -456,7 +483,7 @@ module Wads
456
483
  end
457
484
 
458
485
  def scroll_down
459
- if @current_row < @data_rows.size - 1
486
+ if @current_row + @max_visible_rows < @data_rows.size
460
487
  @current_row = @current_row + @max_visible_rows
461
488
  end
462
489
  end
@@ -546,7 +573,13 @@ module Wads
546
573
  def set_selected_row(mouse_y, column_number)
547
574
  row_number = determine_row_number(mouse_y)
548
575
  if not row_number.nil?
549
- @selected_row = @current_row + row_number
576
+ new_selected_row = @current_row + row_number
577
+ if @selected_row
578
+ if @selected_row == new_selected_row
579
+ return nil # You can't select the same row already selected
580
+ end
581
+ end
582
+ @selected_row = new_selected_row
550
583
  @data_rows[@selected_row][column_number]
551
584
  end
552
585
  end
@@ -702,8 +735,6 @@ module Wads
702
735
  data_set.derive_values(@visible_range, @data_set_hash)
703
736
  data_set.data_points.each do |point|
704
737
  if is_on_screen(point)
705
- #puts "Adding render point at x #{point.x}, #{Time.at(point.x)}"
706
- #puts "Visible range: #{Time.at(@visible_range.left_x)} #{Time.at(@visible_range.right_x)}"
707
738
  data_set.add_rendered_point PlotPoint.new(draw_x(point.x), draw_y(point.y), data_set.color, data_set.data_point_size)
708
739
  end
709
740
  end
@@ -824,4 +855,143 @@ module Wads
824
855
  [get_x_data_val(mouse_x), get_y_data_val(mouse_y)]
825
856
  end
826
857
  end
858
+
859
+ class NodeWidget < Button
860
+ attr_accessor :data_node
861
+
862
+ def initialize(node, x, y, font, border_color = COLOR_DARK_GRAY, text_color = COLOR_HEADER_BRIGHT_BLUE)
863
+ super(node.name, x, y, font, nil, border_color, text_color)
864
+ set_background(COLOR_BLACK)
865
+ @data_node = node
866
+ end
867
+
868
+ def render
869
+ super
870
+ draw_background(Z_ORDER_OVERLAY_BACKGROUND)
871
+ draw_shadow(COLOR_GRAY)
872
+ end
873
+ end
874
+
875
+ class GraphWidget < Widget
876
+ attr_accessor :graph
877
+ attr_accessor :center_node
878
+ attr_accessor :depth
879
+ attr_accessor :selected_node
880
+ attr_accessor :selected_node_x_offset
881
+ attr_accessor :selected_node_y_offset
882
+
883
+ def initialize(x, y, width, height, font, color, graph)
884
+ super x, y, color
885
+ set_font(font)
886
+ set_dimensions(width, height)
887
+ set_border(color)
888
+ @graph = graph
889
+ end
890
+
891
+ def update update_count, mouse_x, mouse_y
892
+ if contains_click(mouse_x, mouse_y) and @selected_node
893
+ @selected_node.x = mouse_x - @selected_node_x_offset
894
+ @selected_node.y = mouse_y - @selected_node_y_offset
895
+ end
896
+ end
897
+
898
+ def button_down id, mouse_x, mouse_y
899
+ if id == Gosu::MsLeft
900
+ # check to see if any node was selected
901
+ if @rendered_nodes
902
+ @rendered_nodes.values.each do |rn|
903
+ if rn.contains_click(mouse_x, mouse_y)
904
+ @selected_node = rn
905
+ @selected_node_x_offset = mouse_x - rn.x
906
+ @selected_node_y_offset = mouse_y - rn.y
907
+ end
908
+ end
909
+ end
910
+ end
911
+ WidgetResult.new(false)
912
+ end
913
+
914
+ def button_up id, mouse_x, mouse_y
915
+ if id == Gosu::MsLeft
916
+ if @selected_node
917
+ @selected_node = nil
918
+ end
919
+ end
920
+ end
921
+
922
+ def set_display(center_node, max_depth)
923
+ # Determine the list of nodes to draw
924
+ @depth = depth
925
+ @visible_data_nodes = @graph.fan_out(center_node, max_depth)
926
+
927
+ # Convert the data nodes to rendered nodes
928
+ # Start by putting the center node in the center, then draw others around it
929
+ @rendered_nodes = {}
930
+ @rendered_nodes[center_node.name] = NodeWidget.new(center_node, center_x, center_y, @font,
931
+ center_node.get_tag("color"), center_node.get_tag("color"))
932
+
933
+ # Spread out the other nodes around the center node
934
+ # going in a circle
935
+ number_of_visible_nodes = @visible_data_nodes.size
936
+ radians_between_nodes = DEG_360 / number_of_visible_nodes.to_f
937
+ current_radians = 0.05
938
+
939
+ @visible_data_nodes.each do |node_name, data_node|
940
+ if node_name == center_node.name
941
+ # skip, we already got this one
942
+ else
943
+ # Use radians to spread the other nodes around the center node
944
+ # For now, we will randomly place them
945
+ node_x = center_x + ((80 + rand(200)) * Math.cos(current_radians))
946
+ node_y = center_y - ((40 + rand(100)) * Math.sin(current_radians))
947
+ if node_x < @x
948
+ node_x = @x + 1
949
+ elsif node_x > right_edge - 20
950
+ node_x = right_edge - 20
951
+ end
952
+ if node_y < @y
953
+ node_y = @y + 1
954
+ elsif node_y > bottom_edge - 26
955
+ node_y = bottom_edge - 26
956
+ end
957
+ current_radians = current_radians + radians_between_nodes
958
+
959
+ # Note we can link between data nodes and rendered nodes using the node name
960
+ # We have a map of each
961
+ @rendered_nodes[data_node.name] = NodeWidget.new(
962
+ data_node,
963
+ node_x,
964
+ node_y,
965
+ @font,
966
+ data_node.get_tag("color"),
967
+ data_node.get_tag("color"))
968
+ end
969
+ end
970
+ end
971
+
972
+ def render
973
+ if @rendered_nodes
974
+ @rendered_nodes.values.each do |vn|
975
+ vn.draw
976
+ end
977
+ # Draw the connections between nodes
978
+ @visible_data_nodes.values.each do |data_node|
979
+ data_node.outputs.each do |connected_data_node|
980
+ if connected_data_node.is_a? Edge
981
+ connected_data_node = connected_data_node.destination
982
+ end
983
+ rendered_node = @rendered_nodes[data_node.name]
984
+ connected_rendered_node = @rendered_nodes[connected_data_node.name]
985
+ if connected_rendered_node.nil?
986
+ # Don't draw if it is not currently visible
987
+ else
988
+ Gosu::draw_line rendered_node.center_x, rendered_node.center_y, rendered_node.color,
989
+ connected_rendered_node.center_x, connected_rendered_node.center_y, connected_rendered_node.color,
990
+ Z_ORDER_GRAPHIC_ELEMENTS
991
+ end
992
+ end
993
+ end
994
+ end
995
+ end
996
+ end
827
997
  end
data/sample_app.rb CHANGED
@@ -10,8 +10,8 @@ class SampleAppCommand
10
10
  command ""
11
11
 
12
12
  example <<~EOS
13
- Run the sample app to analyze stock market data
14
- $ ./run-sample-app
13
+ Run the sample app to analyze stock market data in gui mode
14
+ $ ./run-sample-app -s -g
15
15
  EOS
16
16
 
17
17
  end
@@ -34,6 +34,12 @@ class SampleAppCommand
34
34
  desc "Run sample analysis of lottery numbers"
35
35
  end
36
36
 
37
+ flag :jedi do
38
+ short "-j"
39
+ long "--jedi"
40
+ desc "Run sample analysis of interactions between Star Wars"
41
+ end
42
+
37
43
  flag :gui do
38
44
  short "-g"
39
45
  long "--gui"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wads
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - dbroemme
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-26 00:00:00.000000000 Z
11
+ date: 2021-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gosu
@@ -69,6 +69,7 @@ files:
69
69
  - bin/setup
70
70
  - data/NASDAQ.csv
71
71
  - data/Pick4_12_21_2020.txt
72
+ - data/starwars-episode-4-interactions.json
72
73
  - lib/wads.rb
73
74
  - lib/wads/app.rb
74
75
  - lib/wads/data_structures.rb