mtah-ruby-treemap 0.0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,77 @@
1
+ #
2
+ # image_output.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 'RMagick'
14
+ require File.dirname(__FILE__) + "/output_base"
15
+
16
+ class Treemap::ImageOutput < Treemap::OutputBase
17
+ def initialize
18
+ super
19
+
20
+ # default options for ImageOutput
21
+
22
+ yield self if block_given?
23
+ end
24
+
25
+ def setup_draw
26
+ draw = Magick::Draw.new
27
+ draw.stroke_width(1)
28
+ draw.stroke("#000000")
29
+ draw.stroke_opacity(1)
30
+ draw.fill_opacity(1)
31
+ draw.font_family = "Verdana"
32
+ draw.pointsize = 12
33
+ draw.gravity = Magick::WestGravity
34
+
35
+ return draw
36
+ end
37
+
38
+ def new_image
39
+ Magick::Image.new(@width, @height) {self.background_color = "white"}
40
+ end
41
+
42
+ def to_png(node, filename="treemap.png")
43
+ #
44
+ # XXX Need to flesh out this method. Add in label drawing.
45
+ #
46
+
47
+ image = self.new_image
48
+ draw = self.setup_draw
49
+
50
+ @bounds = self.bounds
51
+
52
+ # Pad for root border
53
+ @bounds.x2 -= 1
54
+ @bounds.y2 -= 1
55
+
56
+ @layout.process(node, @bounds)
57
+
58
+ draw_map(node, draw, image)
59
+
60
+ # render image
61
+ draw.draw(image)
62
+ image.write(filename)
63
+ end
64
+
65
+ def draw_map(node, draw, image)
66
+ return "" if node.nil?
67
+ if(node.color.nil?)
68
+ draw.fill("#CCCCCC")
69
+ else
70
+ draw.fill("#" + @color.get_hex_color(node.color))
71
+ end
72
+ draw.rectangle(node.bounds.x1, node.bounds.y1, node.bounds.x2, node.bounds.y2)
73
+ node.children.each do |c|
74
+ draw_map(c, draw, image)
75
+ end
76
+ end
77
+ end
@@ -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,141 @@
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, :object
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
+ # * :object - An object, that this node might contain.
57
+ #
58
+ #
59
+ def initialize(opts = {})
60
+ @size = opts[:size]
61
+ @label = opts[:label]
62
+ @color = opts[:color]
63
+ @id = opts[:id]
64
+ @object = opts[:object]
65
+ @children = []
66
+
67
+ if(@id.nil?)
68
+ make_id
69
+ end
70
+ end
71
+
72
+ #
73
+ # Returns the depth of the node. 0 for root.
74
+ #
75
+ def depth
76
+ return 0 if parent.nil?
77
+ 1 + self.parent.depth
78
+ end
79
+
80
+ def add_child(node)
81
+ # XXX check to see that a node with the same label doesn't already exist.
82
+ # having 2 nodes with the same label at the same depth
83
+ # doesn't seem to make sense
84
+ node.parent = self
85
+ @children.push(node)
86
+ end
87
+
88
+ #
89
+ # Creates a new node and adds it as a child. See new method.
90
+ #
91
+ def new_child(*args)
92
+ node = Treemap::Node.new(*args)
93
+ self.add_child(node)
94
+ end
95
+
96
+ def find
97
+ @children.find { |c| yield(c) }
98
+ end
99
+
100
+ def to_s
101
+ str = "[:id => " + id + " :label => " + label
102
+ str += " :size => " + size.to_s + " :color => " + color.to_s
103
+ if(not(bounds.nil?))
104
+ str += " :bounds => " + bounds.to_s
105
+ end
106
+ str += "]"
107
+ str
108
+ end
109
+
110
+ def size
111
+ return @size if !@size.nil?
112
+ sum = 0
113
+ @children.each do |c|
114
+ sum += c.size
115
+ end
116
+
117
+ sum
118
+ end
119
+
120
+ # Unscientific formula to calculate the font size depending on the node's area
121
+ def font_size(base_size)
122
+ (base_size * Math.sqrt(self.bounds.width * self.bounds.height) / 125).to_i
123
+ end
124
+
125
+ def label
126
+ return @label if !@label.nil?
127
+ "node - " + size.to_s
128
+ end
129
+
130
+ def leaf?
131
+ return true if @children.nil?
132
+ !(@children.size > 0)
133
+ end
134
+
135
+ private
136
+ def make_id
137
+ #XXX prob should change this. Create a better way to generate unique id's
138
+ @id = MD5.new([self.label, rand(100000000)].join("-")).hexdigest
139
+ end
140
+ end
141
+ 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