gruffy 0.0.2 → 0.0.3
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.
- checksums.yaml +4 -4
- data/lib/gruffy.rb +32 -0
- data/lib/gruffy/accumulator_bar.rb +18 -0
- data/lib/gruffy/area.rb +51 -0
- data/lib/gruffy/bar.rb +108 -0
- data/lib/gruffy/bar_conversion.rb +46 -0
- data/lib/gruffy/base.rb +1201 -0
- data/lib/gruffy/bezier.rb +46 -0
- data/lib/gruffy/bullet.rb +111 -0
- data/lib/gruffy/deprecated.rb +39 -0
- data/lib/gruffy/dot.rb +125 -0
- data/lib/gruffy/line.rb +365 -0
- data/lib/gruffy/mini/bar.rb +37 -0
- data/lib/gruffy/mini/legend.rb +114 -0
- data/lib/gruffy/mini/pie.rb +36 -0
- data/lib/gruffy/mini/side_bar.rb +35 -0
- data/lib/gruffy/net.rb +127 -0
- data/lib/gruffy/photo_bar.rb +100 -0
- data/lib/gruffy/pie.rb +271 -0
- data/lib/gruffy/scatter.rb +314 -0
- data/lib/gruffy/scene.rb +209 -0
- data/lib/gruffy/side_bar.rb +138 -0
- data/lib/gruffy/side_stacked_bar.rb +97 -0
- data/lib/gruffy/spider.rb +125 -0
- data/lib/gruffy/stacked_area.rb +67 -0
- data/lib/gruffy/stacked_bar.rb +61 -0
- data/lib/gruffy/stacked_mixin.rb +23 -0
- data/lib/gruffy/themes.rb +102 -0
- data/lib/gruffy/version.rb +3 -0
- data/rails_generators/gruffy/gruffy_generator.rb +63 -0
- data/rails_generators/gruffy/templates/controller.rb +32 -0
- data/rails_generators/gruffy/templates/functional_test.rb +24 -0
- metadata +33 -2
@@ -0,0 +1,37 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# Makes a small bar graph suitable for display at 200px or even smaller.
|
4
|
+
#
|
5
|
+
module Gruffy
|
6
|
+
module Mini
|
7
|
+
|
8
|
+
class Bar < Gruffy::Bar
|
9
|
+
|
10
|
+
include Gruffy::Mini::Legend
|
11
|
+
|
12
|
+
def initialize_ivars
|
13
|
+
super
|
14
|
+
|
15
|
+
@hide_legend = true
|
16
|
+
@hide_title = true
|
17
|
+
@hide_line_numbers = true
|
18
|
+
|
19
|
+
@marker_font_size = 50.0
|
20
|
+
@minimum_value = 0.0
|
21
|
+
@maximum_value = 0.0
|
22
|
+
@legend_font_size = 60.0
|
23
|
+
end
|
24
|
+
|
25
|
+
def draw
|
26
|
+
expand_canvas_for_vertical_legend
|
27
|
+
|
28
|
+
super
|
29
|
+
|
30
|
+
draw_vertical_legend
|
31
|
+
@d.draw(@base_image)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Gruffy
|
2
|
+
module Mini
|
3
|
+
module Legend
|
4
|
+
|
5
|
+
attr_accessor :hide_mini_legend, :legend_position
|
6
|
+
|
7
|
+
def initialize(*)
|
8
|
+
@hide_mini_legend = false
|
9
|
+
@legend_position = nil
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# The canvas needs to be bigger so we can put the legend beneath it.
|
15
|
+
|
16
|
+
def expand_canvas_for_vertical_legend
|
17
|
+
return if @hide_mini_legend
|
18
|
+
|
19
|
+
@legend_labels = @data.collect {|item| item[Gruffy::Base::DATA_LABEL_INDEX] }
|
20
|
+
|
21
|
+
legend_height = scale_fontsize(
|
22
|
+
@data.length * calculate_line_height +
|
23
|
+
@top_margin + @bottom_margin)
|
24
|
+
|
25
|
+
@original_rows = @raw_rows
|
26
|
+
@original_columns = @raw_columns
|
27
|
+
|
28
|
+
case @legend_position
|
29
|
+
when :right then
|
30
|
+
@rows = [@rows, legend_height].max
|
31
|
+
@columns += calculate_legend_width + @left_margin
|
32
|
+
else
|
33
|
+
@rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
|
34
|
+
end
|
35
|
+
render_background
|
36
|
+
end
|
37
|
+
|
38
|
+
def calculate_line_height
|
39
|
+
calculate_caps_height(@legend_font_size) * 1.7
|
40
|
+
end
|
41
|
+
|
42
|
+
def calculate_legend_width
|
43
|
+
width = @legend_labels.map { |label| calculate_width(@legend_font_size, label) }.max
|
44
|
+
scale_fontsize(width + 40*1.7)
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Draw the legend beneath the existing graph.
|
49
|
+
|
50
|
+
def draw_vertical_legend
|
51
|
+
return if @hide_mini_legend
|
52
|
+
|
53
|
+
legend_square_width = 40.0 # small square with color of this item
|
54
|
+
@legend_left_margin = 100.0
|
55
|
+
legend_top_margin = 40.0
|
56
|
+
|
57
|
+
# May fix legend drawing problem at small sizes
|
58
|
+
@d.font = @font if @font
|
59
|
+
@d.pointsize = @legend_font_size
|
60
|
+
|
61
|
+
case @legend_position
|
62
|
+
when :right then
|
63
|
+
current_x_offset = @original_columns + @left_margin
|
64
|
+
current_y_offset = @top_margin + legend_top_margin
|
65
|
+
else
|
66
|
+
current_x_offset = @legend_left_margin
|
67
|
+
current_y_offset = @original_rows + legend_top_margin
|
68
|
+
end
|
69
|
+
|
70
|
+
debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
|
71
|
+
|
72
|
+
@legend_labels.each_with_index do |legend_label, index|
|
73
|
+
|
74
|
+
# Draw label
|
75
|
+
@d.fill = @font_color
|
76
|
+
@d.font = @font if @font
|
77
|
+
@d.pointsize = scale_fontsize(@legend_font_size)
|
78
|
+
@d.stroke = 'transparent'
|
79
|
+
@d.font_weight = Magick::NormalWeight
|
80
|
+
@d.gravity = Magick::WestGravity
|
81
|
+
@d = @d.annotate_scaled( @base_image,
|
82
|
+
@raw_columns, 1.0,
|
83
|
+
current_x_offset + (legend_square_width * 1.7), current_y_offset,
|
84
|
+
truncate_legend_label(legend_label), @scale)
|
85
|
+
|
86
|
+
# Now draw box with color of this dataset
|
87
|
+
@d = @d.stroke 'transparent'
|
88
|
+
@d = @d.fill @data[index][Gruffy::Base::DATA_COLOR_INDEX]
|
89
|
+
@d = @d.rectangle(current_x_offset,
|
90
|
+
current_y_offset - legend_square_width / 2.0,
|
91
|
+
current_x_offset + legend_square_width,
|
92
|
+
current_y_offset + legend_square_width / 2.0)
|
93
|
+
|
94
|
+
current_y_offset += calculate_line_height
|
95
|
+
end
|
96
|
+
@color_index = 0
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Shorten long labels so they will fit on the canvas.
|
101
|
+
#
|
102
|
+
# Department of Hu...
|
103
|
+
|
104
|
+
def truncate_legend_label(label)
|
105
|
+
truncated_label = label.to_s
|
106
|
+
while calculate_width(scale_fontsize(@legend_font_size), truncated_label) > (@columns - @legend_left_margin - @right_margin) && (truncated_label.length > 1)
|
107
|
+
truncated_label = truncated_label[0..truncated_label.length-2]
|
108
|
+
end
|
109
|
+
truncated_label + (truncated_label.length < label.to_s.length ? "..." : '')
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# Makes a small pie graph suitable for display at 200px or even smaller.
|
4
|
+
#
|
5
|
+
module Gruffy
|
6
|
+
module Mini
|
7
|
+
|
8
|
+
class Pie < Gruffy::Pie
|
9
|
+
|
10
|
+
include Gruffy::Mini::Legend
|
11
|
+
|
12
|
+
def initialize_ivars
|
13
|
+
super
|
14
|
+
|
15
|
+
@hide_legend = true
|
16
|
+
@hide_title = true
|
17
|
+
@hide_line_numbers = true
|
18
|
+
|
19
|
+
@marker_font_size = 60.0
|
20
|
+
@legend_font_size = 60.0
|
21
|
+
end
|
22
|
+
|
23
|
+
def draw
|
24
|
+
expand_canvas_for_vertical_legend
|
25
|
+
|
26
|
+
super
|
27
|
+
|
28
|
+
draw_vertical_legend
|
29
|
+
|
30
|
+
@d.draw(@base_image)
|
31
|
+
end # def draw
|
32
|
+
|
33
|
+
end # class Pie
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
##
|
2
|
+
#
|
3
|
+
# Makes a small pie graph suitable for display at 200px or even smaller.
|
4
|
+
#
|
5
|
+
module Gruffy
|
6
|
+
module Mini
|
7
|
+
|
8
|
+
class SideBar < Gruffy::SideBar
|
9
|
+
|
10
|
+
include Gruffy::Mini::Legend
|
11
|
+
|
12
|
+
def initialize_ivars
|
13
|
+
super
|
14
|
+
@hide_legend = true
|
15
|
+
@hide_title = true
|
16
|
+
@hide_line_numbers = true
|
17
|
+
|
18
|
+
@marker_font_size = 50.0
|
19
|
+
@legend_font_size = 50.0
|
20
|
+
end
|
21
|
+
|
22
|
+
def draw
|
23
|
+
expand_canvas_for_vertical_legend
|
24
|
+
|
25
|
+
super
|
26
|
+
|
27
|
+
draw_vertical_legend
|
28
|
+
|
29
|
+
@d.draw(@base_image)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/gruffy/net.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
# Experimental!!! See also the Spider graph.
|
4
|
+
class Gruffy::Net < Gruffy::Base
|
5
|
+
|
6
|
+
# Hide parts of the graph to fit more datapoints, or for a different appearance.
|
7
|
+
attr_accessor :hide_dots
|
8
|
+
|
9
|
+
# Dimensions of lines and dots; calculated based on dataset size if left unspecified
|
10
|
+
attr_accessor :line_width
|
11
|
+
attr_accessor :dot_radius
|
12
|
+
|
13
|
+
def initialize(*args)
|
14
|
+
super
|
15
|
+
|
16
|
+
@hide_dots = false
|
17
|
+
@hide_line_numbers = true
|
18
|
+
@sorted_drawing = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def draw
|
22
|
+
super
|
23
|
+
|
24
|
+
return unless @has_data
|
25
|
+
|
26
|
+
@radius = @graph_height / 2.0
|
27
|
+
@center_x = @graph_left + (@graph_width / 2.0)
|
28
|
+
@center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
|
29
|
+
|
30
|
+
@x_increment = @graph_width / (@column_count - 1).to_f
|
31
|
+
circle_radius = dot_radius ||
|
32
|
+
clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 2.5), 5.0)
|
33
|
+
|
34
|
+
@d = @d.stroke_opacity 1.0
|
35
|
+
@d = @d.stroke_width line_width ||
|
36
|
+
clip_value_if_greater_than(@columns / (@norm_data.first[DATA_VALUES_INDEX].size * 4), 5.0)
|
37
|
+
|
38
|
+
if defined?(@norm_baseline)
|
39
|
+
level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
|
40
|
+
@d = @d.push
|
41
|
+
@d.stroke_color @baseline_color
|
42
|
+
@d.fill_opacity 0.0
|
43
|
+
@d.stroke_dasharray(10, 20)
|
44
|
+
@d.stroke_width 5
|
45
|
+
@d.line(@graph_left, level, @graph_left + @graph_width, level)
|
46
|
+
@d = @d.pop
|
47
|
+
end
|
48
|
+
|
49
|
+
@norm_data.each do |data_row|
|
50
|
+
@d = @d.stroke data_row[DATA_COLOR_INDEX]
|
51
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
52
|
+
|
53
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
|
54
|
+
next if data_point.nil?
|
55
|
+
|
56
|
+
rad_pos = index * Math::PI * 2 / @column_count
|
57
|
+
point_distance = data_point * @radius
|
58
|
+
start_x = @center_x + Math::sin(rad_pos) * point_distance
|
59
|
+
start_y = @center_y - Math::cos(rad_pos) * point_distance
|
60
|
+
|
61
|
+
next_index = index + 1 < data_row[DATA_VALUES_INDEX].length ? index + 1 : 0
|
62
|
+
|
63
|
+
next_rad_pos = next_index * Math::PI * 2 / @column_count
|
64
|
+
next_point_distance = data_row[DATA_VALUES_INDEX][next_index] * @radius
|
65
|
+
end_x = @center_x + Math::sin(next_rad_pos) * next_point_distance
|
66
|
+
end_y = @center_y - Math::cos(next_rad_pos) * next_point_distance
|
67
|
+
|
68
|
+
@d = @d.line(start_x, start_y, end_x, end_y)
|
69
|
+
|
70
|
+
@d = @d.circle(start_x, start_y, start_x - circle_radius, start_y) unless @hide_dots
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
@d.draw(@base_image)
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# the lines connecting in the center, with the first line vertical
|
80
|
+
def draw_line_markers
|
81
|
+
return if @hide_line_markers
|
82
|
+
|
83
|
+
|
84
|
+
# have to do this here (AGAIN)... see draw() in this class
|
85
|
+
# because this funtion is called before the @radius, @center_x and @center_y are set
|
86
|
+
@radius = @graph_height / 2.0
|
87
|
+
@center_x = @graph_left + (@graph_width / 2.0)
|
88
|
+
@center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
|
89
|
+
|
90
|
+
|
91
|
+
# Draw horizontal line markers and annotate with numbers
|
92
|
+
@d = @d.stroke(@marker_color)
|
93
|
+
@d = @d.stroke_width 1
|
94
|
+
|
95
|
+
|
96
|
+
(0..@column_count-1).each do |index|
|
97
|
+
rad_pos = index * Math::PI * 2 / @column_count
|
98
|
+
|
99
|
+
@d = @d.line(@center_x, @center_y, @center_x + Math::sin(rad_pos) * @radius, @center_y - Math::cos(rad_pos) * @radius)
|
100
|
+
|
101
|
+
|
102
|
+
marker_label = labels[index] ? labels[index].to_s : '000'
|
103
|
+
|
104
|
+
draw_label(@center_x, @center_y, rad_pos * 360 / (2 * Math::PI), @radius, marker_label)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def draw_label(center_x, center_y, angle, radius, amount)
|
111
|
+
r_offset = 1.1
|
112
|
+
x_offset = center_x # + 15 # The label points need to be tweaked slightly
|
113
|
+
y_offset = center_y # + 0 # This one doesn't though
|
114
|
+
x = x_offset + (radius * r_offset * Math.sin(deg2rad(angle)))
|
115
|
+
y = y_offset - (radius * r_offset * Math.cos(deg2rad(angle)))
|
116
|
+
|
117
|
+
# Draw label
|
118
|
+
@d.fill = @marker_color
|
119
|
+
@d.font = @font if @font
|
120
|
+
@d.pointsize = scale_fontsize(20)
|
121
|
+
@d.stroke = 'transparent'
|
122
|
+
@d.font_weight = BoldWeight
|
123
|
+
@d.gravity = CenterGravity
|
124
|
+
@d.annotate_scaled(@base_image, 0, 0, x, y, amount, @scale)
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
# EXPERIMENTAL!
|
4
|
+
#
|
5
|
+
# Doesn't work yet.
|
6
|
+
#
|
7
|
+
class Gruffy::PhotoBar < Gruffy::Base
|
8
|
+
|
9
|
+
# TODO
|
10
|
+
#
|
11
|
+
# define base and cap in yml
|
12
|
+
# allow for image directory to be located elsewhere
|
13
|
+
# more exact measurements for bar heights (go all the way to the bottom of the graph)
|
14
|
+
# option to tile images instead of use a single image
|
15
|
+
# drop base label a few px lower so photo bar graphs can have a base dropping over the lower marker line
|
16
|
+
#
|
17
|
+
|
18
|
+
# The name of a pre-packaged photo-based theme.
|
19
|
+
attr_reader :theme
|
20
|
+
|
21
|
+
# def initialize(target_width=800)
|
22
|
+
# super
|
23
|
+
# init_photo_bar_graphics()
|
24
|
+
# end
|
25
|
+
|
26
|
+
def draw
|
27
|
+
super
|
28
|
+
return unless @has_data
|
29
|
+
|
30
|
+
return # TODO Remove for further development
|
31
|
+
|
32
|
+
init_photo_bar_graphics()
|
33
|
+
|
34
|
+
#Draw#define_clip_path()
|
35
|
+
#Draw#clip_path(pathname)
|
36
|
+
#Draw#composite....with bar graph image OverCompositeOp
|
37
|
+
#
|
38
|
+
# See also
|
39
|
+
#
|
40
|
+
# Draw.pattern # define an image to tile as the filling of a draw object
|
41
|
+
#
|
42
|
+
|
43
|
+
# Setup spacing.
|
44
|
+
#
|
45
|
+
# Columns sit side-by-side.
|
46
|
+
spacing_factor = 0.9
|
47
|
+
@bar_width = @norm_data[0][DATA_COLOR_INDEX].columns
|
48
|
+
|
49
|
+
@norm_data.each_with_index do |data_row, row_index|
|
50
|
+
|
51
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
|
52
|
+
data_point = 0 if data_point.nil?
|
53
|
+
# Use incremented x and scaled y
|
54
|
+
left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
|
55
|
+
left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1
|
56
|
+
right_x = left_x + @bar_width * spacing_factor
|
57
|
+
right_y = @graph_top + @graph_height - 1
|
58
|
+
|
59
|
+
bar_image_width = data_row[DATA_COLOR_INDEX].columns
|
60
|
+
bar_image_height = right_y.to_f - left_y.to_f
|
61
|
+
|
62
|
+
# Crop to scale for data
|
63
|
+
bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height)
|
64
|
+
|
65
|
+
@d.gravity = NorthWestGravity
|
66
|
+
@d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
|
67
|
+
|
68
|
+
# Calculate center based on bar_width and current row
|
69
|
+
label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
|
70
|
+
draw_label(label_center, point_index)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
@d.draw(@base_image)
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
# Return the chosen theme or the default
|
80
|
+
def theme
|
81
|
+
@theme || 'plastik'
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
# Sets up colors with a list of images that will be used.
|
87
|
+
# Images should be 340px tall
|
88
|
+
def init_photo_bar_graphics
|
89
|
+
color_list = Array.new
|
90
|
+
theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme
|
91
|
+
|
92
|
+
Dir.open(theme_dir).each do |file|
|
93
|
+
next unless /\.png$/.match(file)
|
94
|
+
color_list << Image.read("#{theme_dir}/#{file}").first
|
95
|
+
end
|
96
|
+
@colors = color_list
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|