wads 0.1.1 → 0.2.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.
@@ -4,6 +4,7 @@ module Wads
4
4
 
5
5
  SPACER = " "
6
6
  VALUE_WIDTH = 10
7
+ COLOR_TAG = "color"
7
8
 
8
9
  DEG_0 = 0
9
10
  DEG_45 = Math::PI * 0.25
@@ -24,6 +25,9 @@ module Wads
24
25
  DEG_292_5 = DEG_270 + DEG_22_5
25
26
  DEG_337_5 = DEG_315 + DEG_22_5
26
27
 
28
+ #
29
+ # A convenience data structure to store multiple, named sets of key/value pairs
30
+ #
27
31
  class HashOfHashes
28
32
  attr_accessor :data
29
33
 
@@ -31,24 +35,46 @@ module Wads
31
35
  @data = {}
32
36
  end
33
37
 
38
+ #
39
+ # Store the value y based on the key x for the named data set
40
+ #
34
41
  def set(data_set_name, x, y)
35
- data_set = @data[x]
42
+ data_set = @data[data_set_name]
36
43
  if data_set.nil?
37
44
  data_set = {}
38
- @data[x] = data_set
45
+ @data[data_set_name] = data_set
39
46
  end
40
47
  data_set[x] = y
41
48
  end
42
49
 
50
+ #
51
+ # Retrieve the value for the given key x in the named data set
52
+ #
43
53
  def get(data_set_name, x)
44
- data_set = @data[x]
54
+ data_set = @data[data_set_name]
45
55
  if data_set.nil?
46
56
  return nil
47
57
  end
48
58
  data_set[x]
49
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
50
71
  end
51
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
+ #
52
78
  class Stats
53
79
  attr_accessor :name
54
80
  attr_accessor :data
@@ -242,6 +268,20 @@ module Wads
242
268
  end
243
269
  end
244
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
+ #
245
285
  class Node
246
286
  attr_accessor :name
247
287
  attr_accessor :value
@@ -249,6 +289,12 @@ module Wads
249
289
  attr_accessor :outputs
250
290
  attr_accessor :visited
251
291
  attr_accessor :tags
292
+ attr_accessor :depth
293
+
294
+ def id
295
+ # id is an alias for name
296
+ @name
297
+ end
252
298
 
253
299
  def initialize(name, value = nil, tags = {})
254
300
  @name = name
@@ -257,7 +303,20 @@ module Wads
257
303
  @outputs = []
258
304
  @visited = false
259
305
  @tags = tags
306
+ @depth = 1
260
307
  end
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
261
320
 
262
321
  def add_child(name, value)
263
322
  add_output(name, value)
@@ -281,6 +340,35 @@ module Wads
281
340
  edge
282
341
  end
283
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
+
284
372
  def add_tag(key, value)
285
373
  @tags[key] = value
286
374
  end
@@ -313,29 +401,56 @@ module Wads
313
401
  node = node_queue.shift
314
402
  yield node
315
403
  node.outputs.each do |c|
316
- if child.is_a? Edge
317
- child = child.destination
404
+ if c.is_a? Edge
405
+ c = c.destination
318
406
  end
319
407
  node_queue << c
320
408
  end
321
409
  end
322
410
  end
323
411
 
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}"
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
332
442
  end
333
- #@outputs.each do |o|
334
- # puts "Output: #{o.name}"
335
- #end
443
+ end
444
+
445
+ def to_display
446
+ "Node #{@name}: #{value} inputs: #{@backlinks.size} outputs: #{@outputs.size}"
336
447
  end
337
448
  end
338
449
 
450
+ #
451
+ # An Edge is a connection between nodes that stores additional information
452
+ # as arbitrary tags, or name/value pairs.
453
+ #
339
454
  class Edge
340
455
  attr_accessor :destination
341
456
  attr_accessor :tags
@@ -354,13 +469,60 @@ module Wads
354
469
  end
355
470
  end
356
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
+ #
357
480
  class Graph
358
481
  attr_accessor :node_list
359
482
  attr_accessor :node_map
360
483
 
361
- def initialize
484
+ def initialize(root_node = nil)
362
485
  @node_list = []
363
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)
364
526
  end
365
527
 
366
528
  def add_node(node)
@@ -368,13 +530,45 @@ module Wads
368
530
  @node_map[node.name] = node
369
531
  end
370
532
 
371
- def add_edge(source, target, tags)
533
+ def add_edge(source, target, tags = {})
372
534
  if source.is_a? String
373
535
  source = find_node(source)
374
536
  end
375
537
  if target.is_a? String
376
538
  target = find_node(target)
377
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)
378
572
  end
379
573
 
380
574
  def find_node(name)
@@ -388,6 +582,7 @@ module Wads
388
582
  def reset_visited
389
583
  @node_list.each do |node|
390
584
  node.visited = false
585
+ node.depth = 0
391
586
  end
392
587
  end
393
588
 
@@ -401,6 +596,16 @@ module Wads
401
596
  list
402
597
  end
403
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
+
404
609
  def is_cycle(node)
405
610
  reset_visited
406
611
  node.visit do |n|
@@ -413,28 +618,322 @@ module Wads
413
618
  false
414
619
  end
415
620
 
416
- def fan_out(node, max_depth, current_depth = 1)
417
- if current_depth > max_depth
418
- return {}
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
419
626
  end
420
627
  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
628
+ if node.visited
629
+ if current_depth < node.depth
630
+ node.depth = current_depth
426
631
  end
427
- end
632
+ return {}
633
+ else
634
+ map[node.name] = node
635
+ node.depth = current_depth
636
+ node.visited = true
637
+ end
428
638
  node.outputs.each do |child|
429
639
  if child.is_a? Edge
430
640
  child = child.destination
431
641
  end
432
- map_from_child = fan_out(child, max_depth, current_depth + 1)
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)
433
649
  map_from_child.each do |key, value|
434
650
  map[key] = value
435
651
  end
436
652
  end
437
653
  map
438
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)
677
+ #
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
439
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
+
440
939
  end