wads 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -2
- data/README.md +62 -2
- data/data/sample_graph.csv +11 -0
- data/data/starwars-episode-4-interactions.json +336 -0
- data/lib/wads/app.rb +41 -183
- data/lib/wads/data_structures.rb +705 -17
- data/lib/wads/textinput.rb +66 -15
- data/lib/wads/version.rb +1 -1
- data/lib/wads/widgets.rb +2827 -271
- data/lib/wads.rb +1 -1
- data/media/CircleAlpha.png +0 -0
- data/media/CircleAqua.png +0 -0
- data/media/CircleBlue.png +0 -0
- data/media/CircleGray.png +0 -0
- data/media/CircleGreen.png +0 -0
- data/media/CirclePurple.png +0 -0
- data/media/CircleRed.png +0 -0
- data/media/CircleWhite.png +0 -0
- data/media/CircleYellow.png +0 -0
- data/media/SampleGraph.png +0 -0
- data/media/StocksSample.png +0 -0
- data/media/WadsScreenshot.png +0 -0
- data/run-graph +3 -0
- data/run-star-wars +3 -0
- data/run-stocks +3 -0
- data/run-theme-test +3 -0
- data/samples/basic_gosu_with_graph_widget.rb +66 -0
- data/samples/graph.rb +72 -0
- data/samples/star_wars.rb +112 -0
- data/samples/stocks.rb +126 -0
- data/samples/theme_test.rb +256 -0
- metadata +26 -5
- data/run-sample-app +0 -3
- data/sample_app.rb +0 -52
data/lib/wads/data_structures.rb
CHANGED
@@ -4,8 +4,30 @@ module Wads
|
|
4
4
|
|
5
5
|
SPACER = " "
|
6
6
|
VALUE_WIDTH = 10
|
7
|
-
|
7
|
+
COLOR_TAG = "color"
|
8
8
|
|
9
|
+
DEG_0 = 0
|
10
|
+
DEG_45 = Math::PI * 0.25
|
11
|
+
DEG_90 = Math::PI * 0.5
|
12
|
+
DEG_135 = Math::PI * 0.75
|
13
|
+
DEG_180 = Math::PI
|
14
|
+
DEG_225 = Math::PI * 1.25
|
15
|
+
DEG_270 = Math::PI * 1.5
|
16
|
+
DEG_315 = Math::PI * 1.75
|
17
|
+
DEG_360 = Math::PI * 2
|
18
|
+
|
19
|
+
DEG_22_5 = Math::PI * 0.125
|
20
|
+
DEG_67_5 = DEG_45 + DEG_22_5
|
21
|
+
DEG_112_5 = DEG_90 + DEG_22_5
|
22
|
+
DEG_157_5 = DEG_135 + DEG_22_5
|
23
|
+
DEG_202_5 = DEG_180 + DEG_22_5
|
24
|
+
DEG_247_5 = DEG_225 + DEG_22_5
|
25
|
+
DEG_292_5 = DEG_270 + DEG_22_5
|
26
|
+
DEG_337_5 = DEG_315 + DEG_22_5
|
27
|
+
|
28
|
+
#
|
29
|
+
# A convenience data structure to store multiple, named sets of key/value pairs
|
30
|
+
#
|
9
31
|
class HashOfHashes
|
10
32
|
attr_accessor :data
|
11
33
|
|
@@ -13,24 +35,46 @@ module Wads
|
|
13
35
|
@data = {}
|
14
36
|
end
|
15
37
|
|
38
|
+
#
|
39
|
+
# Store the value y based on the key x for the named data set
|
40
|
+
#
|
16
41
|
def set(data_set_name, x, y)
|
17
|
-
data_set = @data[
|
42
|
+
data_set = @data[data_set_name]
|
18
43
|
if data_set.nil?
|
19
44
|
data_set = {}
|
20
|
-
@data[
|
45
|
+
@data[data_set_name] = data_set
|
21
46
|
end
|
22
47
|
data_set[x] = y
|
23
48
|
end
|
24
49
|
|
50
|
+
#
|
51
|
+
# Retrieve the value for the given key x in the named data set
|
52
|
+
#
|
25
53
|
def get(data_set_name, x)
|
26
|
-
data_set = @data[
|
54
|
+
data_set = @data[data_set_name]
|
27
55
|
if data_set.nil?
|
28
56
|
return nil
|
29
57
|
end
|
30
58
|
data_set[x]
|
31
59
|
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Get the list of keys for the named data set
|
63
|
+
#
|
64
|
+
def keys(data_set_name)
|
65
|
+
data_set = @data[data_set_name]
|
66
|
+
if data_set.nil?
|
67
|
+
return nil
|
68
|
+
end
|
69
|
+
data_set.keys
|
70
|
+
end
|
32
71
|
end
|
33
72
|
|
73
|
+
#
|
74
|
+
# Stats allows you to maintain sets of data values, identified by a key,
|
75
|
+
# or data set name. You can then use Stats methods to get the count, average,
|
76
|
+
# sum, or percentiles for these keys.
|
77
|
+
#
|
34
78
|
class Stats
|
35
79
|
attr_accessor :name
|
36
80
|
attr_accessor :data
|
@@ -224,28 +268,672 @@ module Wads
|
|
224
268
|
end
|
225
269
|
end
|
226
270
|
|
271
|
+
#
|
272
|
+
# A node in a graph data structure. Nodes can be used independently, and you
|
273
|
+
# connect them to other nodes, or you can use an overarching Graph instance
|
274
|
+
# to help manage them. Nodes can carry arbitrary metadata in the tags map.
|
275
|
+
# The children are either other nodes, or an Edge instance which can be used
|
276
|
+
# to add information about the connection. For example, in a map graph use
|
277
|
+
# case, the edge may contain information about the distance between the two
|
278
|
+
# nodes. In other applications, metadata about the edges, or connections,
|
279
|
+
# may not be necessary. This class, and the Graph data structure, support
|
280
|
+
# children in either form. Each child connection is a one-directional
|
281
|
+
# connection. The backlinks are stored and managed internally so that we can
|
282
|
+
# easily navigate between nodes of the graph. Nodes themselves have a name
|
283
|
+
# an an optional value.
|
284
|
+
#
|
227
285
|
class Node
|
228
|
-
attr_accessor :x
|
229
|
-
attr_accessor :y
|
230
286
|
attr_accessor :name
|
231
|
-
attr_accessor :
|
232
|
-
attr_accessor :
|
287
|
+
attr_accessor :value
|
288
|
+
attr_accessor :backlinks
|
233
289
|
attr_accessor :outputs
|
234
290
|
attr_accessor :visited
|
291
|
+
attr_accessor :tags
|
292
|
+
attr_accessor :depth
|
235
293
|
|
236
|
-
def
|
237
|
-
|
238
|
-
@
|
239
|
-
|
294
|
+
def id
|
295
|
+
# id is an alias for name
|
296
|
+
@name
|
297
|
+
end
|
298
|
+
|
299
|
+
def initialize(name, value = nil, tags = {})
|
300
|
+
@name = name
|
301
|
+
@value = value
|
302
|
+
@backlinks = []
|
240
303
|
@outputs = []
|
241
304
|
@visited = false
|
305
|
+
@tags = tags
|
306
|
+
@depth = 1
|
242
307
|
end
|
243
308
|
|
309
|
+
def add(name, value = nil, tags = {})
|
310
|
+
add_output_node(Node.new(name, value, tags))
|
311
|
+
end
|
312
|
+
|
313
|
+
def children
|
314
|
+
@outputs
|
315
|
+
end
|
316
|
+
|
317
|
+
def number_of_links
|
318
|
+
@outputs.size + @backlinks.size
|
319
|
+
end
|
320
|
+
|
321
|
+
def add_child(name, value)
|
322
|
+
add_output(name, value)
|
323
|
+
end
|
324
|
+
|
325
|
+
def add_output(name, value)
|
326
|
+
child_node = Node.new(name, value)
|
327
|
+
add_output_node(child_node)
|
328
|
+
end
|
329
|
+
|
330
|
+
def add_output_node(child_node)
|
331
|
+
child_node.backlinks << self
|
332
|
+
@outputs << child_node
|
333
|
+
child_node
|
334
|
+
end
|
335
|
+
|
336
|
+
def add_output_edge(destination, tags = {})
|
337
|
+
edge = Edge.new(destination, tags)
|
338
|
+
destination.backlinks << self
|
339
|
+
@outputs << edge
|
340
|
+
edge
|
341
|
+
end
|
342
|
+
|
343
|
+
def remove_output(name)
|
344
|
+
output_to_delete = nil
|
345
|
+
@outputs.each do |output|
|
346
|
+
if output.is_a? Edge
|
347
|
+
output_node = output.destination
|
348
|
+
if output_node.id == name
|
349
|
+
output_to_delete = output
|
350
|
+
end
|
351
|
+
elsif output.id == name
|
352
|
+
output_to_delete = output
|
353
|
+
end
|
354
|
+
end
|
355
|
+
if output_to_delete
|
356
|
+
@outputs.delete(output_to_delete)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def remove_backlink(name)
|
361
|
+
backlink_to_delete = nil
|
362
|
+
@backlinks.each do |backlink|
|
363
|
+
if backlink.id == name
|
364
|
+
backlink_to_delete = backlink
|
365
|
+
end
|
366
|
+
end
|
367
|
+
if backlink_to_delete
|
368
|
+
@backlinks.delete(backlink_to_delete)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def add_tag(key, value)
|
373
|
+
@tags[key] = value
|
374
|
+
end
|
375
|
+
|
376
|
+
def get_tag(key)
|
377
|
+
@tags[key]
|
378
|
+
end
|
379
|
+
|
380
|
+
def find_node(search_name)
|
381
|
+
if @name == search_name
|
382
|
+
return self
|
383
|
+
end
|
384
|
+
found_node_in_child = nil
|
385
|
+
|
386
|
+
@outputs.each do |child|
|
387
|
+
if child.is_a? Edge
|
388
|
+
child = child.destination
|
389
|
+
end
|
390
|
+
found_node_in_child = child.find_node(search_name)
|
391
|
+
if found_node_in_child
|
392
|
+
return found_node_in_child
|
393
|
+
end
|
394
|
+
end
|
395
|
+
nil
|
396
|
+
end
|
397
|
+
|
398
|
+
def visit(&block)
|
399
|
+
node_queue = [self]
|
400
|
+
until node_queue.empty?
|
401
|
+
node = node_queue.shift
|
402
|
+
yield node
|
403
|
+
node.outputs.each do |c|
|
404
|
+
if c.is_a? Edge
|
405
|
+
c = c.destination
|
406
|
+
end
|
407
|
+
node_queue << c
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
def bfs(max_depth, &block)
|
413
|
+
node_queue = [self]
|
414
|
+
@depth = 1
|
415
|
+
until node_queue.empty?
|
416
|
+
node = node_queue.shift
|
417
|
+
yield node
|
418
|
+
node.visited = true
|
419
|
+
if node.depth < max_depth
|
420
|
+
# Get the set of all outputs and backlinks
|
421
|
+
h = {}
|
422
|
+
node.outputs.each do |n|
|
423
|
+
if n.is_a? Edge
|
424
|
+
n = n.destination
|
425
|
+
end
|
426
|
+
h[n.name] = n
|
427
|
+
end
|
428
|
+
node.backlinks.each do |n|
|
429
|
+
h[n.name] = n
|
430
|
+
end
|
431
|
+
|
432
|
+
h.values.each do |n|
|
433
|
+
if n.visited
|
434
|
+
# ignore, don't process again
|
435
|
+
else
|
436
|
+
n.visited = true
|
437
|
+
n.depth = node.depth + 1
|
438
|
+
node_queue << n
|
439
|
+
end
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def to_display
|
446
|
+
"Node #{@name}: #{value} inputs: #{@backlinks.size} outputs: #{@outputs.size}"
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
#
|
451
|
+
# An Edge is a connection between nodes that stores additional information
|
452
|
+
# as arbitrary tags, or name/value pairs.
|
453
|
+
#
|
454
|
+
class Edge
|
455
|
+
attr_accessor :destination
|
456
|
+
attr_accessor :tags
|
457
|
+
|
458
|
+
def initialize(destination, tags = {})
|
459
|
+
@destination = destination
|
460
|
+
@tags = tags
|
461
|
+
end
|
462
|
+
|
463
|
+
def add_tag(key, value)
|
464
|
+
@tags[key] = value
|
465
|
+
end
|
466
|
+
|
467
|
+
def get_tag(key)
|
468
|
+
@tags[key]
|
469
|
+
end
|
470
|
+
end
|
471
|
+
|
472
|
+
#
|
473
|
+
# A Graph helps manage nodes by providing high level methods to
|
474
|
+
# add or connect nodes to the graph. It also maintains a list of
|
475
|
+
# nodes and supports having multiple root nodes, i.e. nodes with
|
476
|
+
# no incoming connections.
|
477
|
+
# This class also supports constructing the graph from data stored
|
478
|
+
# in a file.
|
479
|
+
#
|
480
|
+
class Graph
|
481
|
+
attr_accessor :node_list
|
482
|
+
attr_accessor :node_map
|
483
|
+
|
484
|
+
def initialize(root_node = nil)
|
485
|
+
@node_list = []
|
486
|
+
@node_map = {}
|
487
|
+
if root_node
|
488
|
+
if root_node.is_a? Node
|
489
|
+
root_node.visit do |n|
|
490
|
+
add_node(n)
|
491
|
+
end
|
492
|
+
elsif root_node.is_a? String
|
493
|
+
read_graph_from_file(root_node)
|
494
|
+
end
|
495
|
+
end
|
496
|
+
end
|
497
|
+
|
498
|
+
def add(name, value = nil, tags = {})
|
499
|
+
add_node(Node.new(name, value, tags))
|
500
|
+
end
|
501
|
+
|
502
|
+
def connect(source, destination, tags = {})
|
503
|
+
add_edge(source, destination)
|
504
|
+
end
|
505
|
+
|
506
|
+
def delete(node)
|
507
|
+
if node.is_a? String
|
508
|
+
node = find_node(node)
|
509
|
+
end
|
510
|
+
node.backlinks.each do |backlink|
|
511
|
+
backlink.remove_output(node.name)
|
512
|
+
end
|
513
|
+
@node_list.delete(node)
|
514
|
+
@node_map.delete(node.name)
|
515
|
+
end
|
516
|
+
|
517
|
+
def disconnect(source, target)
|
518
|
+
if source.is_a? String
|
519
|
+
source = find_node(source)
|
520
|
+
end
|
521
|
+
if target.is_a? String
|
522
|
+
target = find_node(target)
|
523
|
+
end
|
524
|
+
source.remove_output(target.name)
|
525
|
+
target.remove_backlink(source.name)
|
526
|
+
end
|
527
|
+
|
528
|
+
def add_node(node)
|
529
|
+
@node_list << node
|
530
|
+
@node_map[node.name] = node
|
531
|
+
end
|
532
|
+
|
533
|
+
def add_edge(source, target, tags = {})
|
534
|
+
if source.is_a? String
|
535
|
+
source = find_node(source)
|
536
|
+
end
|
537
|
+
if target.is_a? String
|
538
|
+
target = find_node(target)
|
539
|
+
end
|
540
|
+
source.add_output_edge(target, tags)
|
541
|
+
end
|
542
|
+
|
543
|
+
def node_with_most_connections
|
544
|
+
max_node = nil
|
545
|
+
max = -1
|
546
|
+
@node_list.each do |node|
|
547
|
+
num_links = node.number_of_links
|
548
|
+
if num_links > max
|
549
|
+
max = num_links
|
550
|
+
max_node = node
|
551
|
+
end
|
552
|
+
end
|
553
|
+
max_node
|
554
|
+
end
|
555
|
+
|
556
|
+
def get_number_of_connections_range
|
557
|
+
# Find the min and max
|
558
|
+
min = 1000
|
559
|
+
max = 0
|
560
|
+
@node_list.each do |node|
|
561
|
+
num_links = node.number_of_links
|
562
|
+
if num_links < min
|
563
|
+
min = num_links
|
564
|
+
end
|
565
|
+
if num_links > max
|
566
|
+
max = num_links
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
# Then create the scale
|
571
|
+
DataRange.new(min - 0.1, max + 0.1)
|
572
|
+
end
|
573
|
+
|
574
|
+
def find_node(name)
|
575
|
+
@node_map[name]
|
576
|
+
end
|
577
|
+
|
578
|
+
def node_by_index(index)
|
579
|
+
@node_list[index]
|
580
|
+
end
|
581
|
+
|
582
|
+
def reset_visited
|
583
|
+
@node_list.each do |node|
|
584
|
+
node.visited = false
|
585
|
+
node.depth = 0
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
def root_nodes
|
590
|
+
list = []
|
591
|
+
@node_list.each do |node|
|
592
|
+
if node.backlinks.empty?
|
593
|
+
list << node
|
594
|
+
end
|
595
|
+
end
|
596
|
+
list
|
597
|
+
end
|
598
|
+
|
599
|
+
def leaf_nodes
|
600
|
+
list = []
|
601
|
+
@node_list.each do |node|
|
602
|
+
if node.outputs.empty?
|
603
|
+
list << node
|
604
|
+
end
|
605
|
+
end
|
606
|
+
list
|
607
|
+
end
|
608
|
+
|
609
|
+
def is_cycle(node)
|
610
|
+
reset_visited
|
611
|
+
node.visit do |n|
|
612
|
+
if n.visited
|
613
|
+
return true
|
614
|
+
else
|
615
|
+
n.visited = true
|
616
|
+
end
|
617
|
+
end
|
618
|
+
false
|
619
|
+
end
|
620
|
+
|
621
|
+
def traverse_and_collect_nodes(node, max_depth = 0, current_depth = 1)
|
622
|
+
if max_depth > 0
|
623
|
+
if current_depth > max_depth
|
624
|
+
return {}
|
625
|
+
end
|
626
|
+
end
|
627
|
+
map = {}
|
628
|
+
if node.visited
|
629
|
+
if current_depth < node.depth
|
630
|
+
node.depth = current_depth
|
631
|
+
end
|
632
|
+
return {}
|
633
|
+
else
|
634
|
+
map[node.name] = node
|
635
|
+
node.depth = current_depth
|
636
|
+
node.visited = true
|
637
|
+
end
|
638
|
+
node.outputs.each do |child|
|
639
|
+
if child.is_a? Edge
|
640
|
+
child = child.destination
|
641
|
+
end
|
642
|
+
map_from_child = traverse_and_collect_nodes(child, max_depth, current_depth + 1)
|
643
|
+
map_from_child.each do |key, value|
|
644
|
+
map[key] = value
|
645
|
+
end
|
646
|
+
end
|
647
|
+
node.backlinks.each do |child|
|
648
|
+
map_from_child = traverse_and_collect_nodes(child, max_depth, current_depth + 1)
|
649
|
+
map_from_child.each do |key, value|
|
650
|
+
map[key] = value
|
651
|
+
end
|
652
|
+
end
|
653
|
+
map
|
654
|
+
end
|
655
|
+
|
656
|
+
def process_tag_string(tags, tag_string)
|
657
|
+
parts = tag_string.partition("=")
|
658
|
+
tag_name = parts[0]
|
659
|
+
tag_value = parts[2]
|
660
|
+
if tag_name == COLOR_TAG
|
661
|
+
begin
|
662
|
+
value = eval(tag_value)
|
663
|
+
puts "Adding tag #{tag_name} = #{value}"
|
664
|
+
tags[tag_name] = value
|
665
|
+
rescue => e
|
666
|
+
puts "Ignoring tag #{tag_name} = #{tag_value}"
|
667
|
+
end
|
668
|
+
else
|
669
|
+
puts "Adding tag #{tag_name} = #{tag_value}"
|
670
|
+
tags[tag_name] = tag_value
|
671
|
+
end
|
672
|
+
end
|
673
|
+
|
674
|
+
# The format is a csv file as follows:
|
675
|
+
# N,name,value --> nodes
|
676
|
+
# C,source,destination --> connections (also called edges)
|
244
677
|
#
|
245
|
-
#
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
678
|
+
# Optionally, each line type can be followed by comma-separated tag=value
|
679
|
+
def read_graph_from_file(filename)
|
680
|
+
puts "Read graph data from file #{filename}"
|
681
|
+
File.readlines(filename).each do |line|
|
682
|
+
line = line.chomp # remove the carriage return
|
683
|
+
values = line.split(",")
|
684
|
+
type = values[0]
|
685
|
+
tags = {}
|
686
|
+
if type == "N" or type == "n"
|
687
|
+
name = values[1]
|
688
|
+
if values.size > 2
|
689
|
+
value = values[2]
|
690
|
+
# The second position can be a tag or the node value
|
691
|
+
if value.include? "="
|
692
|
+
process_tag_string(tags, value)
|
693
|
+
value = nil
|
694
|
+
end
|
695
|
+
else
|
696
|
+
value = nil
|
697
|
+
end
|
698
|
+
if values.size > 3
|
699
|
+
values[3..-1].each do |tag_string|
|
700
|
+
process_tag_string(tags, tag_string)
|
701
|
+
end
|
702
|
+
end
|
703
|
+
add(name, value, tags)
|
704
|
+
elsif type == "E" or type == "e" or type == "L" or type == "l" or type == "C" or type == "c"
|
705
|
+
source_name = values[1]
|
706
|
+
destination_name = values[2]
|
707
|
+
if values.size > 3
|
708
|
+
values[3..-1].each do |tag_string|
|
709
|
+
process_tag_string(tags, tag_string)
|
710
|
+
end
|
711
|
+
end
|
712
|
+
connect(source_name, destination_name, tags)
|
713
|
+
else
|
714
|
+
puts "Ignoring line: #{line}"
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|
250
718
|
end
|
719
|
+
|
720
|
+
#
|
721
|
+
# An internally used data structure that facilitates walking from the leaf nodes
|
722
|
+
# up to the top of the graph, such that a node is only visited once all of its
|
723
|
+
# descendants have been visited.
|
724
|
+
#
|
725
|
+
class GraphReverseIterator
|
726
|
+
attr_accessor :output
|
727
|
+
def initialize(graph)
|
728
|
+
@output = []
|
729
|
+
graph.root_nodes.each do |root|
|
730
|
+
partial_list = process_node(root)
|
731
|
+
@output.push(*partial_list)
|
732
|
+
end
|
733
|
+
end
|
734
|
+
|
735
|
+
def process_node(node)
|
736
|
+
list = []
|
737
|
+
node.outputs.each do |child_node|
|
738
|
+
if child_node.is_a? Edge
|
739
|
+
child_node = child_node.destination
|
740
|
+
end
|
741
|
+
child_list = process_node(child_node)
|
742
|
+
list.push(*child_list)
|
743
|
+
end
|
744
|
+
|
745
|
+
list << node
|
746
|
+
list
|
747
|
+
end
|
748
|
+
end
|
749
|
+
|
750
|
+
#
|
751
|
+
# A single dimension range, going from min to max.
|
752
|
+
# This class has helper methods to create bins within the given range.
|
753
|
+
#
|
754
|
+
class DataRange
|
755
|
+
attr_accessor :min
|
756
|
+
attr_accessor :max
|
757
|
+
attr_accessor :range
|
758
|
+
|
759
|
+
def initialize(min, max)
|
760
|
+
if min < max
|
761
|
+
@min = min
|
762
|
+
@max = max
|
763
|
+
else
|
764
|
+
@min = max
|
765
|
+
@max = min
|
766
|
+
end
|
767
|
+
@range = @max - @min
|
768
|
+
end
|
769
|
+
|
770
|
+
def bin_max_values(number_of_bins)
|
771
|
+
bin_size = @range / number_of_bins.to_f
|
772
|
+
bins = []
|
773
|
+
bin_start_value = @min
|
774
|
+
number_of_bins.times do
|
775
|
+
bin_start_value = bin_start_value + bin_size
|
776
|
+
bins << bin_start_value
|
777
|
+
end
|
778
|
+
bins
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
#
|
783
|
+
# A two dimensional range used by Plot to determine the visible area
|
784
|
+
# which can be a subset of the total data range(s)
|
785
|
+
#
|
786
|
+
class VisibleRange
|
787
|
+
attr_accessor :left_x
|
788
|
+
attr_accessor :right_x
|
789
|
+
attr_accessor :bottom_y
|
790
|
+
attr_accessor :top_y
|
791
|
+
attr_accessor :x_range
|
792
|
+
attr_accessor :y_range
|
793
|
+
attr_accessor :is_time_based
|
794
|
+
|
795
|
+
def initialize(l, r, b, t, is_time_based = false)
|
796
|
+
if l < r
|
797
|
+
@left_x = l
|
798
|
+
@right_x = r
|
799
|
+
else
|
800
|
+
@left_x = r
|
801
|
+
@right_x = l
|
802
|
+
end
|
803
|
+
if b < t
|
804
|
+
@bottom_y = b
|
805
|
+
@top_y = t
|
806
|
+
else
|
807
|
+
@bottom_y = t
|
808
|
+
@top_y = b
|
809
|
+
end
|
810
|
+
@x_range = @right_x - @left_x
|
811
|
+
@y_range = @top_y - @bottom_y
|
812
|
+
@is_time_based = is_time_based
|
813
|
+
|
814
|
+
@orig_left_x = @left_x
|
815
|
+
@orig_right_x = @right_x
|
816
|
+
@orig_bottom_y = @bottom_y
|
817
|
+
@orig_top_y = @top_y
|
818
|
+
@orig_range_x = @x_range
|
819
|
+
@orig_range_y = @y_range
|
820
|
+
end
|
821
|
+
|
822
|
+
def plus(other_range)
|
823
|
+
l = @left_x < other_range.left_x ? @left_x : other_range.left_x
|
824
|
+
r = @right_x > other_range.right_x ? @right_x : other_range.right_x
|
825
|
+
b = @bottom_y < other_range.bottom_y ? @bottom_y : other_range.bottom_y
|
826
|
+
t = @top_y > other_range.top_y ? @top_y : other_range.top_y
|
827
|
+
VisibleRange.new(l, r, b, t, (@is_time_based or other_range.is_time_based))
|
828
|
+
end
|
829
|
+
|
830
|
+
def x_ten_percent
|
831
|
+
@x_range.to_f / 10
|
832
|
+
end
|
833
|
+
|
834
|
+
def y_ten_percent
|
835
|
+
@y_range.to_f / 10
|
836
|
+
end
|
837
|
+
|
838
|
+
def scale(zoom_level)
|
839
|
+
x_mid_point = @orig_left_x + (@orig_range_x.to_f / 2)
|
840
|
+
x_extension = (@orig_range_x.to_f * zoom_level) / 2
|
841
|
+
@left_x = x_mid_point - x_extension
|
842
|
+
@right_x = x_mid_point + x_extension
|
843
|
+
|
844
|
+
y_mid_point = @orig_bottom_y + (@orig_range_y.to_f / 2)
|
845
|
+
y_extension = (@orig_range_y.to_f * zoom_level) / 2
|
846
|
+
@bottom_y = y_mid_point - y_extension
|
847
|
+
@top_y = y_mid_point + y_extension
|
848
|
+
|
849
|
+
@x_range = @right_x - @left_x
|
850
|
+
@y_range = @top_y - @bottom_y
|
851
|
+
end
|
852
|
+
|
853
|
+
def scroll_up
|
854
|
+
@bottom_y = @bottom_y + y_ten_percent
|
855
|
+
@top_y = @top_y + y_ten_percent
|
856
|
+
@y_range = @top_y - @bottom_y
|
857
|
+
end
|
858
|
+
|
859
|
+
def scroll_down
|
860
|
+
@bottom_y = @bottom_y - y_ten_percent
|
861
|
+
@top_y = @top_y - y_ten_percent
|
862
|
+
@y_range = @top_y - @bottom_y
|
863
|
+
end
|
864
|
+
|
865
|
+
def scroll_right
|
866
|
+
@left_x = @left_x + x_ten_percent
|
867
|
+
@right_x = @right_x + x_ten_percent
|
868
|
+
@x_range = @right_x - @left_x
|
869
|
+
end
|
870
|
+
|
871
|
+
def scroll_left
|
872
|
+
@left_x = @left_x - x_ten_percent
|
873
|
+
@right_x = @right_x - x_ten_percent
|
874
|
+
@x_range = @right_x - @left_x
|
875
|
+
end
|
876
|
+
|
877
|
+
def grid_line_x_values
|
878
|
+
if @cached_grid_line_x_values
|
879
|
+
return @cached_grid_line_x_values
|
880
|
+
end
|
881
|
+
@cached_grid_line_x_values = divide_range_into_values(@x_range, @left_x, @right_x, false)
|
882
|
+
@cached_grid_line_x_values
|
883
|
+
end
|
884
|
+
|
885
|
+
def grid_line_y_values
|
886
|
+
if @cached_grid_line_y_values
|
887
|
+
return @cached_grid_line_y_values
|
888
|
+
end
|
889
|
+
@cached_grid_line_y_values = divide_range_into_values(@y_range, @bottom_y, @top_y, false)
|
890
|
+
@cached_grid_line_y_values
|
891
|
+
end
|
892
|
+
|
893
|
+
def calc_x_values
|
894
|
+
if @cached_calc_x_values
|
895
|
+
return @cached_calc_x_values
|
896
|
+
end
|
897
|
+
@cached_calc_x_values = divide_range_into_values(@x_range, @left_x, @right_x)
|
898
|
+
#puts "The x_axis value to calculate are: #{@cached_calc_x_values}"
|
899
|
+
@cached_calc_x_values
|
900
|
+
end
|
901
|
+
|
902
|
+
def clear_cache
|
903
|
+
@cached_grid_line_x_values = nil
|
904
|
+
@cached_grid_line_y_values = nil
|
905
|
+
@cached_calc_x_values = nil
|
906
|
+
end
|
907
|
+
|
908
|
+
# This method determines what are equidistant points along
|
909
|
+
# the x-axis that we can use to draw gridlines and calculate
|
910
|
+
# derived values from functions
|
911
|
+
def divide_range_into_values(range_size, start_value, end_value, is_derived_values = true)
|
912
|
+
values = []
|
913
|
+
# How big is x-range? What should the step size be?
|
914
|
+
# Generally we want a hundred display points. Let's start there.
|
915
|
+
if range_size < 1.1
|
916
|
+
step_size = is_derived_values ? 0.01 : 0.1
|
917
|
+
elsif range_size < 11
|
918
|
+
step_size = is_derived_values ? 0.1 : 1
|
919
|
+
elsif range_size < 111
|
920
|
+
step_size = is_derived_values ? 1 : 10
|
921
|
+
elsif range_size < 1111
|
922
|
+
step_size = is_derived_values ? 10 : 100
|
923
|
+
elsif range_size < 11111
|
924
|
+
step_size = is_derived_values ? 100 : 1000
|
925
|
+
elsif range_size < 111111
|
926
|
+
step_size = is_derived_values ? 1000 : 10000
|
927
|
+
else
|
928
|
+
step_size = is_derived_values ? 10000 : 100000
|
929
|
+
end
|
930
|
+
grid_x = start_value
|
931
|
+
while grid_x < end_value
|
932
|
+
values << grid_x
|
933
|
+
grid_x = grid_x + step_size
|
934
|
+
end
|
935
|
+
values
|
936
|
+
end
|
937
|
+
end
|
938
|
+
|
251
939
|
end
|