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 +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
|