wads 0.1.0 → 0.2.0

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