gruff 0.2.4 → 0.2.5
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/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 = {
|