wads 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -2
- data/README.md +12 -0
- data/data/starwars-episode-4-interactions.json +336 -0
- data/lib/wads/app.rb +118 -2
- data/lib/wads/data_structures.rb +204 -15
- data/lib/wads/version.rb +1 -1
- data/lib/wads/widgets.rb +185 -15
- data/sample_app.rb +8 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dffb479b6d7bdc47ff103db3f88b1acece5d0ec672361070aec604eb25aa6e9b
|
4
|
+
data.tar.gz: b9fd53c1345bc7f5ab18cd0b5ab2eb74cc0b3276cd538cdae6dad414523f9529
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dbeb4a1dd4f5918d047954b6336219e3dd9429db3de755f56f12f34cf60c4e187d92a967a3cbde88ad02dc35952257da492729f1da762f8b49035775462b4b1b
|
7
|
+
data.tar.gz: 6c28345cfc8658cd25c1243469ff0fa308aa8278597b48e42f9cdd71b1a4d7e285e3df989d19504ea2f3137a36e593ee6a46116a360d4ed92d627c2b9b334619
|
data/CHANGELOG.md
CHANGED
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
|
-
|
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
|
data/lib/wads/data_structures.rb
CHANGED
@@ -4,7 +4,25 @@ module Wads
|
|
4
4
|
|
5
5
|
SPACER = " "
|
6
6
|
VALUE_WIDTH = 10
|
7
|
-
|
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 :
|
232
|
-
attr_accessor :
|
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,
|
237
|
-
@name = name
|
238
|
-
@
|
239
|
-
@
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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
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
|
-
|
32
|
-
|
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 -
|
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
|
-
|
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,
|
127
|
-
Gosu::draw_line @x, @y, color, @x, bottom_edge, color,
|
128
|
-
Gosu::draw_line @x,bottom_edge, color, right_edge, bottom_edge, color,
|
129
|
-
Gosu::draw_line right_edge, @y, color, right_edge, bottom_edge, color,
|
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(
|
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
|
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
|
-
|
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.
|
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-
|
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
|