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
data/lib/gruffy/scene.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
|
2
|
+
require "observer"
|
3
|
+
require File.dirname(__FILE__) + '/base'
|
4
|
+
|
5
|
+
##
|
6
|
+
# A scene is a non-linear graph that assembles layers together to tell a story.
|
7
|
+
# Layers are folders with appropriately named files (see below). You can group
|
8
|
+
# layers and control them together or just set their values individually.
|
9
|
+
#
|
10
|
+
# Examples:
|
11
|
+
#
|
12
|
+
# * A city scene that changes with the time of day and the weather conditions.
|
13
|
+
# * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
|
14
|
+
#
|
15
|
+
# Usage:
|
16
|
+
#
|
17
|
+
# g = Gruffy::Scene.new("500x100", "path/to/city_scene_directory")
|
18
|
+
#
|
19
|
+
# # Define order of layers, back to front
|
20
|
+
# g.layers = %w(background haze sky clouds)
|
21
|
+
#
|
22
|
+
# # Define groups that will be controlled by the same input
|
23
|
+
# g.weather_group = %w(clouds)
|
24
|
+
# g.time_group = %w(background sky)
|
25
|
+
#
|
26
|
+
# # Set values for the layers or groups
|
27
|
+
# g.weather = "cloudy"
|
28
|
+
# g.time = Time.now
|
29
|
+
# g.haze = true
|
30
|
+
#
|
31
|
+
# # Write the final graph to disk
|
32
|
+
# g.write "hazy_daytime_city_scene.png"
|
33
|
+
#
|
34
|
+
#
|
35
|
+
# There are several rules that will magically select a layer when possible.
|
36
|
+
#
|
37
|
+
# * Numbered files will be selected according to the closest value that is less than the input value.
|
38
|
+
# * 'true.png' and 'false.png' will be used as booleans.
|
39
|
+
# * Other named files will be used if the input matches the filename (without the filetype extension).
|
40
|
+
# * If there is a file named 'default.png', it will be used unless other input values are set for the corresponding layer.
|
41
|
+
|
42
|
+
class Gruffy::Scene < Gruffy::Base
|
43
|
+
|
44
|
+
# An array listing the foldernames that will be rendered, from back to front.
|
45
|
+
#
|
46
|
+
# g.layers = %w(sky clouds buildings street people)
|
47
|
+
#
|
48
|
+
attr_reader :layers
|
49
|
+
|
50
|
+
def initialize(target_width, base_dir)
|
51
|
+
@base_dir = base_dir
|
52
|
+
@groups = {}
|
53
|
+
@layers = []
|
54
|
+
super target_width
|
55
|
+
end
|
56
|
+
|
57
|
+
def draw
|
58
|
+
# Join all the custom paths and filter out the empty ones
|
59
|
+
image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
|
60
|
+
images = Magick::ImageList.new(*image_paths)
|
61
|
+
@base_image = images.flatten_images
|
62
|
+
end
|
63
|
+
|
64
|
+
def layers=(ordered_list)
|
65
|
+
ordered_list.each do |layer_name|
|
66
|
+
@layers << Gruffy::Layer.new(@base_dir, layer_name)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Group layers to input values
|
71
|
+
#
|
72
|
+
# g.weather_group = ["sky", "sea", "clouds"]
|
73
|
+
#
|
74
|
+
# Set input values
|
75
|
+
#
|
76
|
+
# g.weather = "cloudy"
|
77
|
+
#
|
78
|
+
def method_missing(method_name, *args)
|
79
|
+
case method_name.to_s
|
80
|
+
when /^(\w+)_group=$/
|
81
|
+
add_group $1, *args
|
82
|
+
return
|
83
|
+
when /^(\w+)=$/
|
84
|
+
set_input $1, args.first
|
85
|
+
return
|
86
|
+
end
|
87
|
+
super
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def add_group(input_name, layer_names)
|
93
|
+
@groups[input_name] = Gruffy::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
|
94
|
+
end
|
95
|
+
|
96
|
+
def set_input(input_name, input_value)
|
97
|
+
if not @groups[input_name].nil?
|
98
|
+
@groups[input_name].send_updates(input_value)
|
99
|
+
else
|
100
|
+
if chosen_layer = @layers.detect { |layer| layer.name == input_name }
|
101
|
+
chosen_layer.update input_value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
class Gruffy::Group
|
110
|
+
|
111
|
+
include Observable
|
112
|
+
attr_reader :name
|
113
|
+
|
114
|
+
def initialize(folder_name, layers)
|
115
|
+
@name = folder_name
|
116
|
+
layers.each do |layer|
|
117
|
+
layer.observe self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def send_updates(value)
|
122
|
+
changed
|
123
|
+
notify_observers value
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
class Gruffy::Layer
|
130
|
+
|
131
|
+
attr_reader :name
|
132
|
+
|
133
|
+
def initialize(base_dir, folder_name)
|
134
|
+
@base_dir = base_dir.to_s
|
135
|
+
@name = folder_name.to_s
|
136
|
+
@filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }.sort
|
137
|
+
@selected_filename = select_default
|
138
|
+
end
|
139
|
+
|
140
|
+
# Register this layer so it receives updates from the group
|
141
|
+
def observe(obj)
|
142
|
+
obj.add_observer self
|
143
|
+
end
|
144
|
+
|
145
|
+
# Choose the appropriate filename for this layer, based on the input
|
146
|
+
def update(value)
|
147
|
+
@selected_filename = case value.to_s
|
148
|
+
when /^(true|false)$/
|
149
|
+
select_boolean value
|
150
|
+
when /^(\w|\s)+$/
|
151
|
+
select_string value
|
152
|
+
when /^-?(\d+\.)?\d+$/
|
153
|
+
select_numeric value
|
154
|
+
when /(\d\d):(\d\d):\d\d/
|
155
|
+
select_time "#{$1}#{$2}"
|
156
|
+
else
|
157
|
+
select_default
|
158
|
+
end
|
159
|
+
# Finally, try to use 'default' if we're still blank
|
160
|
+
@selected_filename ||= select_default
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns the full path to the selected image, or a blank string
|
164
|
+
def path
|
165
|
+
unless @selected_filename.nil? || @selected_filename.empty?
|
166
|
+
return File.join(@base_dir, @name, @selected_filename)
|
167
|
+
end
|
168
|
+
''
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# Match "true.png" or "false.png"
|
174
|
+
def select_boolean(value)
|
175
|
+
file_exists_or_blank value.to_s
|
176
|
+
end
|
177
|
+
|
178
|
+
# Match -5 to _5.png
|
179
|
+
def select_numeric(value)
|
180
|
+
file_exists_or_blank value.to_s.gsub('-', '_')
|
181
|
+
end
|
182
|
+
|
183
|
+
def select_time(value)
|
184
|
+
times = @filenames.map { |filename| filename.gsub('.png', '') }
|
185
|
+
times.each_with_index do |time, index|
|
186
|
+
if (time > value) && (index > 0)
|
187
|
+
return "#{times[index - 1]}.png"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
return "#{times.last}.png"
|
191
|
+
end
|
192
|
+
|
193
|
+
# Match "partly cloudy" to "partly_cloudy.png"
|
194
|
+
def select_string(value)
|
195
|
+
file_exists_or_blank value.to_s.gsub(' ', '_')
|
196
|
+
end
|
197
|
+
|
198
|
+
def select_default
|
199
|
+
@filenames.include?("default.png") ? "default.png" : ''
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns the string "#{filename}.png", if it exists.
|
203
|
+
#
|
204
|
+
# Failing that, it returns default.png, or '' if that doesn't exist.
|
205
|
+
def file_exists_or_blank(filename)
|
206
|
+
@filenames.include?("#{filename}.png") ? "#{filename}.png" : select_default
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
|
3
|
+
##
|
4
|
+
# Graph with individual horizontal bars instead of vertical bars.
|
5
|
+
|
6
|
+
class Gruffy::SideBar < Gruffy::Base
|
7
|
+
|
8
|
+
# Spacing factor applied between bars
|
9
|
+
attr_accessor :bar_spacing
|
10
|
+
|
11
|
+
def draw
|
12
|
+
@has_left_labels = true
|
13
|
+
super
|
14
|
+
|
15
|
+
return unless @has_data
|
16
|
+
draw_bars
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def draw_bars
|
22
|
+
# Setup spacing.
|
23
|
+
#
|
24
|
+
@bar_spacing ||= 0.9
|
25
|
+
|
26
|
+
@bars_width = @graph_height / @column_count.to_f
|
27
|
+
@bar_width = @bars_width / @norm_data.size
|
28
|
+
@d = @d.stroke_opacity 0.0
|
29
|
+
height = Array.new(@column_count, 0)
|
30
|
+
length = Array.new(@column_count, @graph_left)
|
31
|
+
padding = (@bar_width * (1 - @bar_spacing)) / 2
|
32
|
+
|
33
|
+
# if we're a side stacked bar then we don't need to draw ourself at all
|
34
|
+
# because sometimes (due to different heights/min/max) you can actually
|
35
|
+
# see both graphs and it looks like crap
|
36
|
+
return if self.is_a?(Gruffy::SideStackedBar)
|
37
|
+
|
38
|
+
@norm_data.each_with_index do |data_row, row_index|
|
39
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
40
|
+
|
41
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
|
42
|
+
|
43
|
+
# Using the original calcs from the stacked bar chart
|
44
|
+
# to get the difference between
|
45
|
+
# part of the bart chart we wish to stack.
|
46
|
+
temp1 = @graph_left + (@graph_width - data_point * @graph_width - height[point_index])
|
47
|
+
temp2 = @graph_left + @graph_width - height[point_index]
|
48
|
+
difference = temp2 - temp1
|
49
|
+
|
50
|
+
left_x = length[point_index] - 1
|
51
|
+
left_y = @graph_top + (@bars_width * point_index) + (@bar_width * row_index) + padding
|
52
|
+
right_x = left_x + difference
|
53
|
+
right_y = left_y + @bar_width * @bar_spacing
|
54
|
+
|
55
|
+
height[point_index] += (data_point * @graph_width)
|
56
|
+
|
57
|
+
@d = @d.rectangle(left_x, left_y, right_x, right_y)
|
58
|
+
|
59
|
+
# Calculate center based on bar_width and current row
|
60
|
+
|
61
|
+
if @use_data_label
|
62
|
+
label_center = @graph_top + (@bar_width * (row_index+point_index) + @bar_width / 2)
|
63
|
+
draw_label(label_center, row_index, @norm_data[row_index][DATA_LABEL_INDEX])
|
64
|
+
else
|
65
|
+
label_center = @graph_top + (@bars_width * point_index + @bars_width / 2)
|
66
|
+
draw_label(label_center, point_index)
|
67
|
+
end
|
68
|
+
if @show_labels_for_bar_values
|
69
|
+
val = (@label_formatting || '%.2f') % @norm_data[row_index][3][point_index]
|
70
|
+
draw_value_label(right_x+40, (@graph_top + (((row_index+point_index+1) * @bar_width) - (@bar_width / 2)))-12, val.commify, true)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
@d.draw(@base_image)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Instead of base class version, draws vertical background lines and label
|
80
|
+
def draw_line_markers
|
81
|
+
|
82
|
+
return if @hide_line_markers
|
83
|
+
|
84
|
+
@d = @d.stroke_antialias false
|
85
|
+
|
86
|
+
# Draw horizontal line markers and annotate with numbers
|
87
|
+
@d = @d.stroke(@marker_color)
|
88
|
+
@d = @d.stroke_width 1
|
89
|
+
number_of_lines = @marker_count || 5
|
90
|
+
number_of_lines = 1 if number_of_lines == 0
|
91
|
+
|
92
|
+
# TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
|
93
|
+
increment = significant(@spread.to_f / number_of_lines)
|
94
|
+
(0..number_of_lines).each do |index|
|
95
|
+
|
96
|
+
line_diff = (@graph_right - @graph_left) / number_of_lines
|
97
|
+
x = @graph_right - (line_diff * index) - 1
|
98
|
+
@d = @d.line(x, @graph_bottom, x, @graph_top)
|
99
|
+
diff = index - number_of_lines
|
100
|
+
marker_label = diff.abs * increment + @minimum_value
|
101
|
+
|
102
|
+
unless @hide_line_numbers
|
103
|
+
@d.fill = @font_color
|
104
|
+
@d.font = @font if @font
|
105
|
+
@d.stroke = 'transparent'
|
106
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
107
|
+
@d.gravity = CenterGravity
|
108
|
+
# TODO Center text over line
|
109
|
+
@d = @d.annotate_scaled(@base_image,
|
110
|
+
0, 0, # Width of box to draw text in
|
111
|
+
x, @graph_bottom + (LABEL_MARGIN * 2.0), # Coordinates of text
|
112
|
+
marker_label.to_s, @scale)
|
113
|
+
end # unless
|
114
|
+
@d = @d.stroke_antialias true
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Draw on the Y axis instead of the X
|
120
|
+
|
121
|
+
def draw_label(y_offset, index, label=nil)
|
122
|
+
if !@labels[index].nil? && @labels_seen[index].nil?
|
123
|
+
lbl = (@use_data_label) ? label : @labels[index]
|
124
|
+
@d.fill = @font_color
|
125
|
+
@d.font = @font if @font
|
126
|
+
@d.stroke = 'transparent'
|
127
|
+
@d.font_weight = NormalWeight
|
128
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
129
|
+
@d.gravity = EastGravity
|
130
|
+
@d = @d.annotate_scaled(@base_image,
|
131
|
+
1, 1,
|
132
|
+
-@graph_left + LABEL_MARGIN * 2.0, y_offset,
|
133
|
+
lbl, @scale)
|
134
|
+
@labels_seen[index] = 1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base'
|
2
|
+
require File.dirname(__FILE__) + '/side_bar'
|
3
|
+
require File.dirname(__FILE__) + '/stacked_mixin'
|
4
|
+
|
5
|
+
##
|
6
|
+
# New gruffy graph type added to enable sideways stacking bar charts
|
7
|
+
# (basically looks like a x/y flip of a standard stacking bar chart)
|
8
|
+
#
|
9
|
+
# alun.eyre@googlemail.com
|
10
|
+
|
11
|
+
class Gruffy::SideStackedBar < Gruffy::SideBar
|
12
|
+
include StackedMixin
|
13
|
+
|
14
|
+
# Spacing factor applied between bars
|
15
|
+
attr_accessor :bar_spacing
|
16
|
+
|
17
|
+
def draw
|
18
|
+
@has_left_labels = true
|
19
|
+
get_maximum_by_stack
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def draw_bars
|
26
|
+
# Setup spacing.
|
27
|
+
#
|
28
|
+
# Columns sit stacked.
|
29
|
+
@bar_spacing ||= 0.9
|
30
|
+
|
31
|
+
@bar_width = @graph_height / @column_count.to_f
|
32
|
+
@d = @d.stroke_opacity 0.0
|
33
|
+
height = Array.new(@column_count, 0)
|
34
|
+
length = Array.new(@column_count, @graph_left)
|
35
|
+
padding = (@bar_width * (1 - @bar_spacing)) / 2
|
36
|
+
if @show_labels_for_bar_values
|
37
|
+
label_values = Array.new
|
38
|
+
0.upto(@column_count-1) {|i| label_values[i] = {:value => 0, :right_x => 0}}
|
39
|
+
end
|
40
|
+
@norm_data.each_with_index do |data_row, row_index|
|
41
|
+
data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
|
42
|
+
|
43
|
+
## using the original calcs from the stacked bar chart to get the difference between
|
44
|
+
## part of the bart chart we wish to stack.
|
45
|
+
temp1 = @graph_left + (@graph_width -
|
46
|
+
data_point * @graph_width -
|
47
|
+
height[point_index]) + 1
|
48
|
+
temp2 = @graph_left + @graph_width - height[point_index] - 1
|
49
|
+
difference = temp2 - temp1
|
50
|
+
|
51
|
+
@d = @d.fill data_row[DATA_COLOR_INDEX]
|
52
|
+
|
53
|
+
left_x = length[point_index] #+ 1
|
54
|
+
left_y = @graph_top + (@bar_width * point_index) + padding
|
55
|
+
right_x = left_x + difference
|
56
|
+
right_y = left_y + @bar_width * @bar_spacing
|
57
|
+
length[point_index] += difference
|
58
|
+
height[point_index] += (data_point * @graph_width - 2)
|
59
|
+
|
60
|
+
if @show_labels_for_bar_values
|
61
|
+
label_values[point_index][:value] += @norm_data[row_index][3][point_index]
|
62
|
+
label_values[point_index][:right_x] = right_x
|
63
|
+
end
|
64
|
+
|
65
|
+
# if a data point is 0 it can result in weird really thing lines
|
66
|
+
# that shouldn't even be there being drawn on top of the existing
|
67
|
+
# bar - this is bad
|
68
|
+
if data_point != 0
|
69
|
+
@d = @d.rectangle(left_x, left_y, right_x, right_y)
|
70
|
+
# Calculate center based on bar_width and current row
|
71
|
+
end
|
72
|
+
# we still need to draw the labels
|
73
|
+
# Calculate center based on bar_width and current row
|
74
|
+
label_center = @graph_top + (@bar_width * point_index) + (@bar_width * @bar_spacing / 2.0)
|
75
|
+
draw_label(label_center, point_index)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
if @show_labels_for_bar_values
|
80
|
+
label_values.each_with_index do |data, i|
|
81
|
+
val = (@label_formatting || "%.2f") % data[:value]
|
82
|
+
draw_value_label(data[:right_x]+40, (@graph_top + (((i+1) * @bar_width) - (@bar_width / 2)))-12, val.commify, true)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
@d.draw(@base_image)
|
87
|
+
end
|
88
|
+
|
89
|
+
def larger_than_max?(data_point, index=0)
|
90
|
+
max(data_point, index) > @maximum_value
|
91
|
+
end
|
92
|
+
|
93
|
+
def max(data_point, index)
|
94
|
+
@data.inject(0) {|sum, item| sum + item[DATA_VALUES_INDEX][index]}
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
|
2
|
+
require File.dirname(__FILE__) + '/base'
|
3
|
+
|
4
|
+
# Experimental!!! See also the Net graph.
|
5
|
+
#
|
6
|
+
# Submitted by Kevin Clark http://glu.ttono.us/
|
7
|
+
class Gruffy::Spider < Gruffy::Base
|
8
|
+
|
9
|
+
# Hide all text
|
10
|
+
attr_reader :hide_text
|
11
|
+
attr_accessor :hide_axes
|
12
|
+
attr_reader :transparent_background
|
13
|
+
attr_accessor :rotation
|
14
|
+
|
15
|
+
def transparent_background=(value)
|
16
|
+
@transparent_background = value
|
17
|
+
@base_image = render_transparent_background if value
|
18
|
+
end
|
19
|
+
|
20
|
+
def hide_text=(value)
|
21
|
+
@hide_title = @hide_text = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def initialize(max_value, target_width = 800)
|
25
|
+
super(target_width)
|
26
|
+
@max_value = max_value
|
27
|
+
@hide_legend = true
|
28
|
+
@rotation = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
def draw
|
32
|
+
@hide_line_markers = true
|
33
|
+
|
34
|
+
super
|
35
|
+
|
36
|
+
return unless @has_data
|
37
|
+
|
38
|
+
# Setup basic positioning
|
39
|
+
radius = @graph_height / 2.0
|
40
|
+
center_x = @graph_left + (@graph_width / 2.0)
|
41
|
+
center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
|
42
|
+
|
43
|
+
@unit_length = radius / @max_value
|
44
|
+
|
45
|
+
additive_angle = (2 * Math::PI)/ @data.size
|
46
|
+
|
47
|
+
# Draw axes
|
48
|
+
draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes
|
49
|
+
|
50
|
+
# Draw polygon
|
51
|
+
draw_polygon(center_x, center_y, additive_angle)
|
52
|
+
|
53
|
+
@d.draw(@base_image)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def normalize_points(value)
|
59
|
+
value * @unit_length
|
60
|
+
end
|
61
|
+
|
62
|
+
def draw_label(center_x, center_y, angle, radius, amount)
|
63
|
+
r_offset = 50 # The distance out from the center of the pie to get point
|
64
|
+
x_offset = center_x # The label points need to be tweaked slightly
|
65
|
+
y_offset = center_y + 0 # This one doesn't though
|
66
|
+
x = x_offset + ((radius + r_offset) * Math.cos(angle))
|
67
|
+
y = y_offset + ((radius + r_offset) * Math.sin(angle))
|
68
|
+
|
69
|
+
# Draw label
|
70
|
+
@d.fill = @marker_color
|
71
|
+
@d.font = @font if @font
|
72
|
+
@d.pointsize = scale_fontsize(legend_font_size)
|
73
|
+
@d.stroke = 'transparent'
|
74
|
+
@d.font_weight = BoldWeight
|
75
|
+
@d.gravity = CenterGravity
|
76
|
+
@d.annotate_scaled( @base_image,
|
77
|
+
0, 0,
|
78
|
+
x, y,
|
79
|
+
amount, @scale)
|
80
|
+
end
|
81
|
+
|
82
|
+
def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil)
|
83
|
+
return if hide_axes
|
84
|
+
|
85
|
+
current_angle = rotation * Math::PI / 180.0
|
86
|
+
|
87
|
+
@data.each do |data_row|
|
88
|
+
@d.stroke(line_color || data_row[DATA_COLOR_INDEX])
|
89
|
+
@d.stroke_width 5.0
|
90
|
+
|
91
|
+
x_offset = radius * Math.cos(current_angle)
|
92
|
+
y_offset = radius * Math.sin(current_angle)
|
93
|
+
|
94
|
+
@d.line(center_x, center_y,
|
95
|
+
center_x + x_offset,
|
96
|
+
center_y + y_offset)
|
97
|
+
|
98
|
+
draw_label(center_x, center_y, current_angle, radius, data_row[DATA_LABEL_INDEX].to_s) unless hide_text
|
99
|
+
|
100
|
+
current_angle += additive_angle
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def draw_polygon(center_x, center_y, additive_angle, color = nil)
|
105
|
+
points = []
|
106
|
+
current_angle = rotation * Math::PI / 180.0
|
107
|
+
|
108
|
+
@data.each do |data_row|
|
109
|
+
points << center_x + normalize_points(data_row[DATA_VALUES_INDEX].first) * Math.cos(current_angle)
|
110
|
+
points << center_y + normalize_points(data_row[DATA_VALUES_INDEX].first) * Math.sin(current_angle)
|
111
|
+
current_angle += additive_angle
|
112
|
+
end
|
113
|
+
|
114
|
+
@d.stroke_width 1.0
|
115
|
+
@d.stroke(color || @marker_color)
|
116
|
+
@d.fill(color || @marker_color)
|
117
|
+
@d.fill_opacity 0.4
|
118
|
+
@d.polygon(*points)
|
119
|
+
end
|
120
|
+
|
121
|
+
def sums_for_spider
|
122
|
+
@data.inject(0.0) {|sum, data_row| sum + data_row[DATA_VALUES_INDEX].first}
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|