ruby-treemap 0.0.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.
@@ -0,0 +1,30 @@
1
+ #--
2
+ # layout_base.rb - RubyTreemap
3
+ #
4
+ # Copyright (c) 2006 by Andrew Bruno <aeb@qnot.org>
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ #++
12
+
13
+ module Treemap
14
+ class LayoutBase
15
+ attr_accessor :position, :color
16
+
17
+ def initialize
18
+ # Similar to the css style position. If set to :fixed x,y bounds calculations
19
+ # should be computed relative to the root bounds. If set to :absolute then they
20
+ # should be computed relative to the parent bounds.
21
+ # See http://www.w3.org/TR/CSS2/visuren.html#positioning-scheme
22
+ @position = :fixed
23
+ yield self if block_given?
24
+ end
25
+
26
+ # Subclasses will override
27
+ def process(node, bounds)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,134 @@
1
+ #
2
+ # node.rb - RubyTreemap
3
+ #
4
+ # Copyright (c) 2006 by Andrew Bruno <aeb@qnot.org>
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ #
12
+
13
+ require "md5"
14
+
15
+ module Treemap
16
+ #
17
+ # A generic tree node class which is used to represent the data to be
18
+ # treemap'ed. The Layout and Output classes expect an object of this
19
+ # type to perform the treemap calculations on.
20
+ #
21
+ # Create a simple tree:
22
+ # root = Treemap::Node.new
23
+ # root.new_child(:size => 6)
24
+ # root.new_child(:size => 6)
25
+ # root.new_child(:size => 4)
26
+ # root.new_child(:size => 3)
27
+ # root.new_child(:size => 2)
28
+ # root.new_child(:size => 2)
29
+ # root.new_child(:size => 1)
30
+ #
31
+ # Initialize values:
32
+ # root = Treemap::Node.new(:label => "All", :size => 100, :color => "#FFCCFF")
33
+ # child1 = Treemap::Node.new(:label => "Child 1", :size => 50)
34
+ # child2 = Treemap::Node.new(:label => "Child 2", :size => 50)
35
+ # root.add_child(child1)
36
+ # root.add_child(child2)
37
+ #
38
+ #
39
+ class Treemap::Node
40
+ attr_accessor :id, :label, :color, :size, :bounds, :parent
41
+ attr_reader :children
42
+
43
+ #
44
+ # Create a new Node. You can initialize the node by passing in
45
+ # a hash with any of the following keys:
46
+ #
47
+ # * :size - The size that this node represents. For non-leaf nodes the
48
+ # size must be equal to the sum of the sizes of it's children. If size
49
+ # is nil then the value will be calculated by recursing the children.
50
+ # * :label - The label for this node. Used when displaying. Defaults to "node"
51
+ # * :color - The background fill color in hex to render when drawing the
52
+ # square. If the value is a number a color will be calculated. An example
53
+ # string color would be: ##FFFFFF (white)
54
+ # * :id - a unique id to assign to this node. Default id will be generated if
55
+ # one is not provided.
56
+ #
57
+ #
58
+ def initialize(opts = {})
59
+ @size = opts[:size]
60
+ @label = opts[:label]
61
+ @color = opts[:color]
62
+ @id = opts[:id]
63
+ @children = []
64
+
65
+ if(@id.nil?)
66
+ make_id
67
+ end
68
+ end
69
+
70
+ #
71
+ # Returns the depth of the node. 0 for root.
72
+ #
73
+ def depth
74
+ return 0 if parent.nil?
75
+ 1 + self.parent.depth
76
+ end
77
+
78
+ def add_child(node)
79
+ # XXX check to see that a node with the same label doesn't already exist.
80
+ # having 2 nodes with the same label at the same depth
81
+ # doesn't seem to make sense
82
+ node.parent = self
83
+ @children.push(node)
84
+ end
85
+
86
+ #
87
+ # Creates a new node and adds it as a child. See new method.
88
+ #
89
+ def new_child(*args)
90
+ node = Treemap::Node.new(*args)
91
+ self.add_child(node)
92
+ end
93
+
94
+ def find
95
+ @children.find { |c| yield(c) }
96
+ end
97
+
98
+ def to_s
99
+ str = "[:id => " + id + " :label => " + label
100
+ str += " :size => " + size.to_s + " :color => " + color.to_s
101
+ if(not(bounds.nil?))
102
+ str += " :bounds => " + bounds.to_s
103
+ end
104
+ str += "]"
105
+ str
106
+ end
107
+
108
+ def size
109
+ return @size if !@size.nil?
110
+ sum = 0
111
+ @children.each do |c|
112
+ sum += c.size
113
+ end
114
+
115
+ sum
116
+ end
117
+
118
+ def label
119
+ return @label if !@label.nil?
120
+ "node - " + size.to_s
121
+ end
122
+
123
+ def leaf?
124
+ return true if @children.nil?
125
+ !(@children.size > 0)
126
+ end
127
+
128
+ private
129
+ def make_id
130
+ #XXX prob should change this. Create a better way to generate unique id's
131
+ @id = MD5.new([self.label, rand(100000000)].join("-")).hexdigest
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,39 @@
1
+ #
2
+ # output_base.rb - RubyTreemap
3
+ #
4
+ # Copyright (c) 2006 by Andrew Bruno <aeb@qnot.org>
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ #
12
+
13
+ require 'rexml/document'
14
+
15
+ module Treemap
16
+ class OutputBase
17
+ attr_accessor(:width, :height, :layout, :color, :margin_top, :margin_left)
18
+
19
+ def initialize
20
+ @width = 800
21
+ @height = 600
22
+ @margin_top = 0
23
+ @margin_left = 0
24
+ @layout = Treemap::SquarifiedLayout.new
25
+ @color = Treemap::GradientColor.new
26
+ yield self if block_given?
27
+ end
28
+
29
+ protected
30
+ def bounds
31
+ x1 = self.margin_left
32
+ y1 = self.margin_top
33
+ x2 = self.width + self.margin_left
34
+ y2 = self.height + self.margin_top
35
+ bounds = Treemap::Rectangle.new(x1, y1, x2, y2)
36
+ return bounds
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ #
2
+ # rectangle.rb - RubyTreemap
3
+ #
4
+ # Copyright (c) 2006 by Andrew Bruno <aeb@qnot.org>
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ #
12
+
13
+ module Treemap
14
+ class Rectangle
15
+ attr_accessor :x1, :y1, :x2, :y2
16
+
17
+ def initialize(x1, y1, x2, y2)
18
+ @x1 = x1
19
+ @y1 = y1
20
+ @x2 = x2
21
+ @y2 = y2
22
+
23
+ yield self if block_given?
24
+ end
25
+
26
+ def to_s
27
+ "[" + [@x1, @y1, @x2, @y2].join(",") + "]"
28
+ end
29
+
30
+ def width
31
+ @x2 - @x1
32
+ end
33
+
34
+ def height
35
+ @y2 - @y1
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,97 @@
1
+ #
2
+ # slice_layout.rb - RubyTreemap
3
+ #
4
+ # Copyright (c) 2006 by Andrew Bruno <aeb@qnot.org>
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ #
12
+
13
+ require File.dirname(__FILE__) + "/layout_base"
14
+
15
+ class Treemap::SliceLayout < Treemap::LayoutBase
16
+ Vertical = 1
17
+ Horizontal = 2
18
+
19
+ def process(node, bounds, axis=nil)
20
+ bounds = bounds.clone
21
+
22
+ node.bounds = bounds.clone
23
+
24
+ if(@position == :absolute)
25
+ bounds.x2 = bounds.width
26
+ bounds.y2 = bounds.height
27
+ bounds.x1 = 0
28
+ bounds.y1 = 0
29
+ end
30
+
31
+ if(!node.leaf?)
32
+ process_children(node.children, bounds, axis)
33
+ end
34
+ end
35
+
36
+ def sum(children)
37
+ sum = 0
38
+ children.each do |c|
39
+ sum += c.size
40
+ end
41
+ sum
42
+ end
43
+
44
+ def process_children(children, bounds, axis=nil)
45
+ parent_bounds = bounds.clone
46
+ bounds = bounds.clone
47
+
48
+ axis = axis(bounds) if(axis.nil?)
49
+
50
+ width = axis == Vertical ? bounds.width : bounds.height
51
+
52
+ sum = sum(children)
53
+
54
+ # XXX should we sort? seems to produce better map but not tested on
55
+ # larger data sets
56
+ # children.sort {|c1,c2| c2.size <=> c1.size}.each do |c|
57
+ children.each do |c|
58
+ size = ((c.size.to_f / sum.to_f)*width).round
59
+
60
+ if(axis == Vertical)
61
+ bounds.x2 = bounds.x1 + size
62
+ else
63
+ bounds.y2 = bounds.y1 + size
64
+ end
65
+
66
+ process(c, bounds, flip(axis))
67
+
68
+ axis == Vertical ? bounds.x1 = bounds.x2 : bounds.y1 = bounds.y2
69
+ end
70
+
71
+ last = children.last
72
+ if(axis == Vertical)
73
+ last.bounds.x2 = parent_bounds.x2
74
+ else
75
+ last.bounds.y2 = parent_bounds.y2
76
+ end
77
+
78
+ end
79
+
80
+ def flip(axis)
81
+ return Horizontal if axis == Vertical
82
+ Vertical
83
+ end
84
+
85
+ def vertical?(bounds)
86
+ bounds.width > bounds.height
87
+ end
88
+
89
+ def horizontal?(bounds)
90
+ bounds.width < bounds.height
91
+ end
92
+
93
+ def axis(bounds)
94
+ return Horizontal if horizontal?(bounds)
95
+ Vertical
96
+ end
97
+ end
@@ -0,0 +1,141 @@
1
+ #
2
+ # squarified_layout.rb - RubyTreemap
3
+ #
4
+ # Copyright (c) 2006 by Andrew Bruno <aeb@qnot.org>
5
+ #
6
+ # This program is free software; you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation; either version 2 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ #
12
+
13
+ require File.dirname(__FILE__) + "/layout_base"
14
+
15
+ class Treemap::SquarifiedLayout < Treemap::SliceLayout
16
+
17
+ def process(node, bounds, axis=nil)
18
+
19
+ bounds = bounds.clone
20
+
21
+ node.bounds = bounds.clone
22
+
23
+ if(@position == :absolute)
24
+ bounds.x2 = bounds.width
25
+ bounds.y2 = bounds.height
26
+ bounds.x1 = 0
27
+ bounds.y1 = 0
28
+ end
29
+
30
+ if(!node.leaf?)
31
+ squarify_children(node, bounds, flip(axis))
32
+ end
33
+ end
34
+
35
+ def squarify_children(node, bounds, axis)
36
+
37
+ parent_bounds = bounds.clone
38
+ bounds = bounds.clone
39
+
40
+ node.children.sort! {|a,b| b.size <=> a.size}
41
+
42
+ if(node.children.size < 2)
43
+ process_children(node.children, bounds, flip(axis))
44
+ end
45
+
46
+ parent_size = node.size
47
+ first_child = node.children.first
48
+
49
+ row_size = first_child.size
50
+ row_max = row_size.to_f / parent_size.to_f
51
+ total = row_max
52
+
53
+ prev_aspect = aspect_ratio(bounds, first_child.size.to_f / row_size.to_f, total, axis)
54
+ row = [first_child]
55
+
56
+ node.children[1 .. node.children.size-1].each do |c|
57
+ child_prop = c.size.to_f / parent_size.to_f
58
+ aspect = aspect_ratio(bounds, c.size.to_f / row_size.to_f, total + child_prop, axis)
59
+
60
+ if(aspect > prev_aspect)
61
+ newb = bounds.clone
62
+ if(axis == Vertical)
63
+ newb.x2 = bounds.x1 + ((bounds.width * total)).round
64
+ else
65
+ newb.y2 = bounds.y1 + ((bounds.height * total)).round
66
+ end
67
+
68
+ process_children(row, newb, flip(axis))
69
+
70
+ if(axis == Vertical)
71
+ bounds.x1 = newb.x2
72
+ else
73
+ bounds.y1 = newb.y2
74
+ end
75
+
76
+ axis = flip(axis)
77
+ parent_size -= row_size
78
+ row_size = c.size
79
+ total = row_max = row_size.to_f / parent_size.to_f
80
+ prev_aspect = aspect_ratio(bounds, c.size.to_f / row_size.to_f, total, axis)
81
+ row = [c]
82
+ else
83
+ row_size += c.size
84
+ total += child_prop
85
+ prev_aspect = aspect
86
+ row.push(c)
87
+ end
88
+ end
89
+
90
+ process_children(row, bounds, flip(axis))
91
+ end
92
+
93
+ def aspect_ratio(bounds, node_prop, row_prop, axis)
94
+ height = bounds.height * row_prop
95
+ width = bounds.width * node_prop
96
+ if(axis == Vertical)
97
+ width = bounds.width * row_prop
98
+ height = bounds.height * node_prop
99
+ end
100
+
101
+ return 0 if width == 0 and height == 0
102
+
103
+ a = 0;
104
+ b = 0;
105
+ if(width > 0)
106
+ a = height.to_f / width.to_f
107
+ end
108
+ if(height > 0)
109
+ b = width.to_f / height.to_f
110
+ end
111
+
112
+ ratio = [a, b].max
113
+
114
+ ratio
115
+ end
116
+
117
+ def axis(bounds)
118
+ # XXX experiment with switching
119
+ # axis = super(bounds)
120
+ # flip(axis)
121
+ end
122
+
123
+ # XXX another way of computing the aspect ratio
124
+ def aspect_ratio_method2(bounds, max, proportion, axis)
125
+
126
+ large = bounds.height
127
+ small = bounds.width
128
+ if(axis == Vertical)
129
+ large = bounds.width
130
+ small = bounds.height
131
+ end
132
+
133
+ ratio = (large * proportion).to_f / ((small * max).to_f / proportion.to_f).to_f
134
+
135
+ if(ratio < 1)
136
+ ratio = 1.to_f / ratio.to_f
137
+ end
138
+
139
+ ratio
140
+ end
141
+ end