ruby-treemap 0.0.1

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