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.
- data/COPYING +340 -0
- data/ChangeLog +7 -0
- data/EXAMPLES +48 -0
- data/README +48 -0
- data/Rakefile +71 -0
- data/TODO +6 -0
- data/lib/treemap.rb +66 -0
- data/lib/treemap/color_base.rb +61 -0
- data/lib/treemap/gradient_color.rb +71 -0
- data/lib/treemap/html_output.rb +162 -0
- data/lib/treemap/image_output.rb +77 -0
- data/lib/treemap/layout_base.rb +30 -0
- data/lib/treemap/node.rb +134 -0
- data/lib/treemap/output_base.rb +39 -0
- data/lib/treemap/rectangle.rb +38 -0
- data/lib/treemap/slice_layout.rb +97 -0
- data/lib/treemap/squarified_layout.rb +141 -0
- data/lib/treemap/svg_output.rb +96 -0
- data/test/tc_color.rb +17 -0
- data/test/tc_html.rb +22 -0
- data/test/tc_simple.rb +30 -0
- data/test/tc_slice.rb +25 -0
- data/test/tc_squarified.rb +25 -0
- data/test/tc_svg.rb +21 -0
- data/test/test_base.rb +39 -0
- metadata +73 -0
@@ -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
|
data/lib/treemap/node.rb
ADDED
@@ -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
|