wads 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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