mtah-ruby-treemap 0.0.3.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,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