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.
@@ -4,8 +4,30 @@ module Wads
4
4
 
5
5
  SPACER = " "
6
6
  VALUE_WIDTH = 10
7
- NODE_UNKNOWN = "undefined"
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[x]
42
+ data_set = @data[data_set_name]
18
43
  if data_set.nil?
19
44
  data_set = {}
20
- @data[x] = data_set
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[x]
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 :type
232
- attr_accessor :inputs
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 initialize(name, type = NODE_UNKNOWN)
237
- @name = name
238
- @type = type
239
- @inputs = []
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
- # TODO Visitor pattern and solution for detecting cyclic graphs
246
- #
247
- # when you visit, reset all the visited flags
248
- # set it to true when you visit the node
249
- # first check though if visited already true, if so, you have a cycle
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