gruff 0.2.4 → 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +9 -0
- data/lib/gruff.rb +20 -12
- data/lib/gruff/bar.rb +8 -2
- data/lib/gruff/bar_conversion.rb +7 -16
- data/lib/gruff/base.rb +128 -106
- data/lib/gruff/line.rb +11 -15
- data/lib/gruff/mini/bar.rb +32 -0
- data/lib/gruff/mini/legend.rb +77 -0
- data/lib/gruff/mini/pie.rb +32 -0
- data/lib/gruff/mini/side_bar.rb +22 -0
- data/lib/gruff/pie.rb +26 -15
- data/lib/gruff/side_stacked_bar.rb +1 -0
- data/test/gruff_test_case.rb +86 -4
- data/test/test_bar.rb +27 -0
- data/test/test_legend.rb +40 -40
- data/test/test_line.rb +9 -39
- data/test/test_mini_bar.rb +18 -0
- data/test/test_mini_pie.rb +11 -0
- data/test/test_mini_side_bar.rb +12 -0
- data/test/test_pie.rb +14 -0
- data/test/test_sidestacked_bar.rb +15 -2
- metadata +13 -5
data/lib/gruff/line.rb
CHANGED
@@ -41,12 +41,7 @@ class Gruff::Line < Gruff::Base
|
|
41
41
|
|
42
42
|
# Check to see if more than one datapoint was given. NaN can result otherwise.
|
43
43
|
@x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
|
44
|
-
|
45
|
-
circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
|
46
|
-
|
47
|
-
@d = @d.stroke_opacity 1.0
|
48
|
-
@d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
|
49
|
-
|
44
|
+
|
50
45
|
if (defined?(@norm_baseline)) then
|
51
46
|
level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
|
52
47
|
@d = @d.push
|
@@ -58,10 +53,8 @@ class Gruff::Line < Gruff::Base
|
|
58
53
|
@d = @d.pop
|
59
54
|
end
|
60
55
|
|
61
|
-
@norm_data.each do |data_row|
|
56
|
+
@norm_data.each do |data_row|
|
62
57
|
prev_x = prev_y = nil
|
63
|
-
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
64
|
-
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
65
58
|
|
66
59
|
data_row[1].each_with_index do |data_point, index|
|
67
60
|
new_x = @graph_left + (@x_increment * index)
|
@@ -71,9 +64,16 @@ class Gruff::Line < Gruff::Base
|
|
71
64
|
|
72
65
|
new_y = @graph_top + (@graph_height - data_point * @graph_height)
|
73
66
|
|
74
|
-
|
67
|
+
# Reset each time to avoid thin-line errors
|
68
|
+
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
69
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
70
|
+
@d = @d.stroke_opacity 1.0
|
71
|
+
@d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
|
72
|
+
|
73
|
+
if !@hide_lines and !prev_x.nil? and !prev_y.nil? then
|
75
74
|
@d = @d.line(prev_x, prev_y, new_x, new_y)
|
76
75
|
end
|
76
|
+
circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
|
77
77
|
@d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
|
78
78
|
|
79
79
|
prev_x = new_x
|
@@ -86,13 +86,9 @@ class Gruff::Line < Gruff::Base
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def normalize
|
89
|
-
@maximum_value =
|
89
|
+
@maximum_value = [@maximum_value.to_f, @baseline_value.to_f].max
|
90
90
|
super
|
91
91
|
@norm_baseline = (@baseline_value.to_f / @maximum_value.to_f) if @baseline_value
|
92
92
|
end
|
93
93
|
|
94
|
-
def max(a, b)
|
95
|
-
(a < b ? b : a)
|
96
|
-
end
|
97
|
-
|
98
94
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# Makes a small bar graph suitable for display at 200px or even smaller.
|
4
|
+
#
|
5
|
+
module Gruff
|
6
|
+
module Mini
|
7
|
+
|
8
|
+
class Bar < Gruff::Bar
|
9
|
+
|
10
|
+
include Gruff::Mini::Legend
|
11
|
+
|
12
|
+
def draw
|
13
|
+
@hide_legend = true
|
14
|
+
@hide_title = true
|
15
|
+
@hide_line_numbers = true
|
16
|
+
|
17
|
+
@marker_font_size = 40.0
|
18
|
+
@minimum_value = 0.0
|
19
|
+
@legend_font_size = 60.0
|
20
|
+
|
21
|
+
expand_canvas_for_vertical_legend
|
22
|
+
|
23
|
+
super
|
24
|
+
|
25
|
+
draw_vertical_legend
|
26
|
+
@d.draw(@base_image)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Gruff
|
2
|
+
module Mini
|
3
|
+
module Legend
|
4
|
+
|
5
|
+
##
|
6
|
+
# The canvas needs to be bigger so we can put the legend beneath it.
|
7
|
+
|
8
|
+
def expand_canvas_for_vertical_legend
|
9
|
+
@original_rows = @raw_rows
|
10
|
+
@rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
|
11
|
+
render_background
|
12
|
+
end
|
13
|
+
|
14
|
+
##
|
15
|
+
# Draw the legend beneath the existing graph.
|
16
|
+
|
17
|
+
def draw_vertical_legend
|
18
|
+
|
19
|
+
@legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] }
|
20
|
+
|
21
|
+
legend_square_width = 40.0 # small square with color of this item
|
22
|
+
legend_square_margin = 10.0
|
23
|
+
@legend_left_margin = 40.0
|
24
|
+
legend_top_margin = 40.0
|
25
|
+
|
26
|
+
# May fix legend drawing problem at small sizes
|
27
|
+
@d.font = @font if @font
|
28
|
+
@d.pointsize = @legend_font_size
|
29
|
+
|
30
|
+
current_x_offset = @graph_left + @legend_left_margin
|
31
|
+
current_y_offset = @original_rows + legend_top_margin
|
32
|
+
|
33
|
+
debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
|
34
|
+
|
35
|
+
@legend_labels.each_with_index do |legend_label, index|
|
36
|
+
|
37
|
+
# Draw label
|
38
|
+
@d.fill = @font_color
|
39
|
+
@d.font = @font if @font
|
40
|
+
@d.pointsize = scale_fontsize(@legend_font_size)
|
41
|
+
@d.stroke = 'transparent'
|
42
|
+
@d.font_weight = Magick::NormalWeight
|
43
|
+
@d.gravity = Magick::WestGravity
|
44
|
+
@d = @d.annotate_scaled( @base_image,
|
45
|
+
@raw_columns, 1.0,
|
46
|
+
current_x_offset + (legend_square_width * 1.7), current_y_offset,
|
47
|
+
truncate_legend_label(legend_label), @scale)
|
48
|
+
|
49
|
+
# Now draw box with color of this dataset
|
50
|
+
@d = @d.stroke 'transparent'
|
51
|
+
@d = @d.fill @data[index][Gruff::Base::DATA_COLOR_INDEX]
|
52
|
+
@d = @d.rectangle(current_x_offset,
|
53
|
+
current_y_offset - legend_square_width / 2.0,
|
54
|
+
current_x_offset + legend_square_width,
|
55
|
+
current_y_offset + legend_square_width / 2.0)
|
56
|
+
|
57
|
+
current_y_offset += calculate_caps_height(@legend_font_size) * 1.7
|
58
|
+
end
|
59
|
+
@color_index = 0
|
60
|
+
end
|
61
|
+
|
62
|
+
##
|
63
|
+
# Shorten long labels so they will fit on the canvas.
|
64
|
+
#
|
65
|
+
# Department of Hu...
|
66
|
+
|
67
|
+
def truncate_legend_label(label)
|
68
|
+
truncated_label = label.to_s
|
69
|
+
while calculate_width(scale_fontsize(@legend_font_size), truncated_label) > (@columns - @legend_left_margin - Gruff::Base::RIGHT_MARGIN) && (truncated_label.length > 1)
|
70
|
+
truncated_label = truncated_label[0..truncated_label.length-2]
|
71
|
+
end
|
72
|
+
truncated_label + (truncated_label.length < label.to_s.length ? "…" : '')
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# Makes a small pie graph suitable for display at 200px or even smaller.
|
4
|
+
#
|
5
|
+
module Gruff
|
6
|
+
module Mini
|
7
|
+
|
8
|
+
class Pie < Gruff::Pie
|
9
|
+
|
10
|
+
include Gruff::Mini::Legend
|
11
|
+
|
12
|
+
def draw
|
13
|
+
@hide_legend = true
|
14
|
+
@hide_title = true
|
15
|
+
@hide_line_numbers = true
|
16
|
+
|
17
|
+
@marker_font_size = 40.0
|
18
|
+
@legend_font_size = 60.0
|
19
|
+
|
20
|
+
expand_canvas_for_vertical_legend
|
21
|
+
|
22
|
+
super
|
23
|
+
|
24
|
+
draw_vertical_legend
|
25
|
+
|
26
|
+
@d.draw(@base_image)
|
27
|
+
end # def draw
|
28
|
+
|
29
|
+
end # class Pie
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# Makes a small pie graph suitable for display at 200px or even smaller.
|
4
|
+
#
|
5
|
+
module Gruff
|
6
|
+
module Mini
|
7
|
+
|
8
|
+
class SideBar < Gruff::Base
|
9
|
+
|
10
|
+
def draw
|
11
|
+
@hide_legend = true
|
12
|
+
@hide_title = true
|
13
|
+
@hide_line_numbers = true
|
14
|
+
|
15
|
+
@marker_font_size = 40.0
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/gruff/pie.rb
CHANGED
@@ -3,6 +3,8 @@ require File.dirname(__FILE__) + '/base'
|
|
3
3
|
|
4
4
|
class Gruff::Pie < Gruff::Base
|
5
5
|
|
6
|
+
TEXT_OFFSET_PERCENTAGE = 0.15
|
7
|
+
|
6
8
|
def draw
|
7
9
|
@hide_line_markers = true
|
8
10
|
|
@@ -19,13 +21,13 @@ class Gruff::Pie < Gruff::Base
|
|
19
21
|
prev_degrees = 0.0
|
20
22
|
|
21
23
|
# Use full data since we can easily calculate percentages
|
22
|
-
@data.each do |data_row|
|
23
|
-
if data_row[
|
24
|
+
@data.sort{ |a, b| a[DATA_VALUES_INDEX][0] <=> b[DATA_VALUES_INDEX][0] }.each do |data_row|
|
25
|
+
if data_row[DATA_VALUES_INDEX][0] > 0
|
24
26
|
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
25
27
|
@d = @d.fill 'transparent'
|
26
28
|
@d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
|
27
29
|
|
28
|
-
current_degrees = (data_row[
|
30
|
+
current_degrees = (data_row[DATA_VALUES_INDEX][0] / total_sum) * 360.0
|
29
31
|
|
30
32
|
# ellipse will draw the the stroke centered on the first two parameters offset by the second two.
|
31
33
|
# therefore, in order to draw a circle of the proper diameter we must center the stroke at
|
@@ -35,33 +37,42 @@ class Gruff::Pie < Gruff::Base
|
|
35
37
|
prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
|
36
38
|
|
37
39
|
half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
|
38
|
-
|
39
40
|
|
41
|
+
# End the string with %% to escape the single %.
|
42
|
+
# RMagick must use sprintf with the string and % has special significance.
|
43
|
+
label_string = ((data_row[DATA_VALUES_INDEX][0] / total_sum) * 100.0).round.to_s + '%%'
|
40
44
|
@d = draw_label(center_x,center_y,
|
41
|
-
half_angle,
|
42
|
-
|
43
|
-
((data_row[1][0] / total_sum) * 100).round.to_s + '% ')
|
45
|
+
half_angle, radius + (radius * TEXT_OFFSET_PERCENTAGE),
|
46
|
+
label_string)
|
44
47
|
|
45
48
|
prev_degrees += current_degrees
|
46
49
|
end
|
47
50
|
end
|
48
51
|
|
52
|
+
# TODO debug a circle where the text is drawn...
|
53
|
+
|
49
54
|
@d.draw(@base_image)
|
50
55
|
end
|
51
56
|
|
52
57
|
private
|
53
58
|
|
59
|
+
##
|
60
|
+
# Labels are drawn around a slightly wider ellipse to give room for
|
61
|
+
# labels on the left and right.
|
54
62
|
def draw_label(center_x, center_y, angle, radius, amount)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
63
|
+
# TODO Don't use so many hard-coded numbers
|
64
|
+
r_offset = 5.0 # The distance out from the center of the pie to get point
|
65
|
+
x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
|
66
|
+
y_offset = center_y # This one doesn't though
|
67
|
+
radius_offset = (radius + r_offset)
|
68
|
+
ellipse_factor = radius_offset * 0.10
|
69
|
+
x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle.deg2rad))
|
70
|
+
y = y_offset + (radius_offset * Math.sin(angle.deg2rad))
|
60
71
|
|
61
72
|
# Draw label
|
62
|
-
@d.fill = @
|
73
|
+
@d.fill = @font_color
|
63
74
|
@d.font = @font if @font
|
64
|
-
@d.pointsize = scale_fontsize(
|
75
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
65
76
|
@d.stroke = 'transparent'
|
66
77
|
@d.font_weight = BoldWeight
|
67
78
|
@d.gravity = CenterGravity
|
@@ -73,7 +84,7 @@ private
|
|
73
84
|
|
74
85
|
def sums_for_pie
|
75
86
|
total_sum = 0.0
|
76
|
-
@data.collect {|data_row| total_sum += data_row[
|
87
|
+
@data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX][0] }
|
77
88
|
total_sum
|
78
89
|
end
|
79
90
|
|
data/test/gruff_test_case.rb
CHANGED
@@ -2,9 +2,54 @@ $:.unshift(File.dirname(__FILE__) + "/../lib/")
|
|
2
2
|
|
3
3
|
require 'test/unit'
|
4
4
|
require 'gruff'
|
5
|
+
require 'test_timer'
|
5
6
|
|
6
7
|
class GruffTestCase < Test::Unit::TestCase
|
7
8
|
|
9
|
+
def setup
|
10
|
+
@datasets = [
|
11
|
+
[:Jimmy, [25, 36, 86, 39, 25, 31, 79, 88]],
|
12
|
+
[:Charles, [80, 54, 67, 54, 68, 70, 90, 95]],
|
13
|
+
[:Julie, [22, 29, 35, 38, 36, 40, 46, 57]],
|
14
|
+
[:Jane, [95, 95, 95, 90, 85, 80, 88, 100]],
|
15
|
+
[:Philip, [90, 34, 23, 12, 78, 89, 98, 88]],
|
16
|
+
["Arthur", [5, 10, 13, 11, 6, 16, 22, 32]],
|
17
|
+
]
|
18
|
+
|
19
|
+
@labels = {
|
20
|
+
0 => '5/6',
|
21
|
+
1 => '5/15',
|
22
|
+
2 => '5/24',
|
23
|
+
3 => '5/30',
|
24
|
+
4 => '6/4',
|
25
|
+
5 => '6/12',
|
26
|
+
6 => '6/21',
|
27
|
+
7 => '6/28',
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def setup_single_dataset
|
32
|
+
@datasets = [
|
33
|
+
[:Jimmy, [25, 36, 86]]
|
34
|
+
]
|
35
|
+
|
36
|
+
@labels = {
|
37
|
+
0 => 'You',
|
38
|
+
1 => 'Average',
|
39
|
+
2 => 'Lifetime'
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def setup_wide_dataset
|
44
|
+
@datasets = [
|
45
|
+
["Auto", 25],
|
46
|
+
["Food", 5],
|
47
|
+
["Entertainment", 15]
|
48
|
+
]
|
49
|
+
|
50
|
+
@labels = { 0 => 'This Month' }
|
51
|
+
end
|
52
|
+
|
8
53
|
def test_dummy
|
9
54
|
assert true
|
10
55
|
end
|
@@ -19,12 +64,13 @@ protected
|
|
19
64
|
# g.data('students', [1, 2, 3, 4])
|
20
65
|
# end
|
21
66
|
#
|
22
|
-
def graph_sized(filename,
|
67
|
+
def graph_sized(filename, sizes=['', 400])
|
23
68
|
class_name = self.class.name.gsub(/^TestGruff/, '')
|
24
|
-
|
69
|
+
Array(sizes).each do |size|
|
25
70
|
g = instance_eval("Gruff::#{class_name}.new #{size}")
|
26
|
-
|
27
|
-
|
71
|
+
g.title = "#{class_name} Graph"
|
72
|
+
yield g
|
73
|
+
write_test_file g, "#{filename}_#{size}.png"
|
28
74
|
end
|
29
75
|
end
|
30
76
|
|
@@ -32,4 +78,40 @@ protected
|
|
32
78
|
graph.write(File.dirname(__FILE__) + "/output/#{filename}")
|
33
79
|
end
|
34
80
|
|
81
|
+
##
|
82
|
+
# Example:
|
83
|
+
#
|
84
|
+
# setup_basic_graph Gruff::Pie, 400
|
85
|
+
#
|
86
|
+
def setup_basic_graph(*args)
|
87
|
+
klass, size = Gruff::Bar, 400
|
88
|
+
# Allow args to be klass, size or just klass or just size.
|
89
|
+
#
|
90
|
+
# TODO Refactor
|
91
|
+
case args.length
|
92
|
+
when 1
|
93
|
+
case args[0]
|
94
|
+
when Fixnum
|
95
|
+
size = args[0]
|
96
|
+
klass = eval("Gruff::#{self.class.name.gsub(/^TestGruff/, '')}")
|
97
|
+
when String
|
98
|
+
size = args[0]
|
99
|
+
klass = eval("Gruff::#{self.class.name.gsub(/^TestGruff/, '')}")
|
100
|
+
else
|
101
|
+
klass = args[0]
|
102
|
+
end
|
103
|
+
when 2
|
104
|
+
klass, size = args[0], args[1]
|
105
|
+
end
|
106
|
+
|
107
|
+
g = klass.new(size)
|
108
|
+
g.title = "My Bar Graph"
|
109
|
+
g.labels = @labels
|
110
|
+
|
111
|
+
@datasets.each do |data|
|
112
|
+
g.data(data[0], data[1])
|
113
|
+
end
|
114
|
+
g
|
115
|
+
end
|
116
|
+
|
35
117
|
end
|
data/test/test_bar.rb
CHANGED
@@ -120,6 +120,32 @@ class TestGruffBar < GruffTestCase
|
|
120
120
|
g.write("test/output/bar_x_y_labels.png")
|
121
121
|
end
|
122
122
|
|
123
|
+
def test_fireball
|
124
|
+
g = Gruff::Bar.new
|
125
|
+
g.hide_title = true
|
126
|
+
g.hide_line_numbers = true
|
127
|
+
|
128
|
+
running_totals = []
|
129
|
+
new_additions = []
|
130
|
+
|
131
|
+
(0..19).each do |i|
|
132
|
+
add = rand(10)
|
133
|
+
case i
|
134
|
+
when 0
|
135
|
+
running_totals << 0
|
136
|
+
new_additions << add
|
137
|
+
else
|
138
|
+
running_totals << new_additions[i-1]
|
139
|
+
new_additions << running_totals[i-1] + add
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
g.data '1', running_totals
|
144
|
+
g.data '2', new_additions
|
145
|
+
|
146
|
+
g.write("test/output/bar_fireball.png")
|
147
|
+
end
|
148
|
+
|
123
149
|
|
124
150
|
def test_wide_graph
|
125
151
|
g = setup_basic_graph('800x400')
|
@@ -221,6 +247,7 @@ class TestGruffBar < GruffTestCase
|
|
221
247
|
g.theme = {
|
222
248
|
:colors => %w(#efd250 #666699 #e5573f #9595e2),
|
223
249
|
:marker_color => 'white',
|
250
|
+
:font_color => 'blue',
|
224
251
|
:background_image => "assets/pc306715.jpg"
|
225
252
|
}
|
226
253
|
g.labels = {
|