gruff 0.1.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/CocoaMagick.rb +311 -0
- data/lib/gruff/bar.rb +15 -4
- data/lib/gruff/base.rb +263 -100
- data/lib/gruff/deprecated.rb +39 -0
- data/lib/gruff/line.rb +4 -2
- data/lib/gruff/pie.rb +1 -1
- data/lib/gruff/scene.rb +8 -7
- data/test/bar_test.rb +95 -39
- data/test/gruff_test_case.rb +22 -0
- data/test/line_test.rb +47 -1
- data/test/pie_test.rb +12 -0
- data/test/scene_test.rb +6 -0
- metadata +4 -2
data/lib/CocoaMagick.rb
ADDED
@@ -0,0 +1,311 @@
|
|
1
|
+
# CocoaMagick.rb
|
2
|
+
# Tim Burks / RubyCocoa Resources http://www.rubycocoa.com
|
3
|
+
#
|
4
|
+
# This is a quick-and-dirty replacement for RMagic written specifically
|
5
|
+
# for Geoffrey Grosenbach's "gruff" graphing package.
|
6
|
+
# It uses RubyCocoa and the Cocoa API to draw the elements of gruff graphs.
|
7
|
+
# It is FAR from comprehensive, but works for nearly all of the test cases
|
8
|
+
# distributed with gruff. Tested with gruff 0.1.2.
|
9
|
+
#
|
10
|
+
# To use, change "require 'RMagick'"to "require 'CocoaMagick'" in gruff/base.rb.
|
11
|
+
# You also must have RubyCocoa installed, preferably the most recent version.
|
12
|
+
# This means you must also be running on a Mac, preferably with OS X 10.4 or later.
|
13
|
+
# Due to a Cocoa limitation, this script must either be run by the user currently
|
14
|
+
# logged into the console or as root.
|
15
|
+
#
|
16
|
+
# For help with RubyCocoa, visit "RubyCocoa Resources" at www.rubycocoa.com.
|
17
|
+
# For more on gruff, see http://nubyonrails.com/pages/gruff
|
18
|
+
#
|
19
|
+
require 'osx/cocoa'
|
20
|
+
|
21
|
+
def log(str)
|
22
|
+
puts str if false
|
23
|
+
end
|
24
|
+
|
25
|
+
class OSX::NSImage
|
26
|
+
def writePNG(filename)
|
27
|
+
bits = OSX::NSBitmapImageRep.alloc.initWithData(self.TIFFRepresentation)
|
28
|
+
data = bits.representationUsingType_properties(OSX::NSPNGFileType, nil)
|
29
|
+
data.writeToFile_atomically(filename, false)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class OSX::NSColor
|
34
|
+
def self.colorWithName(name)
|
35
|
+
log "colorWithName #{name}"
|
36
|
+
if name[0..0] == "#"
|
37
|
+
r = eval("0x"+name[1..2]) / 256.0
|
38
|
+
g = eval("0x"+name[3..4]) / 256.0
|
39
|
+
b = eval("0x"+name[5..6]) / 256.0
|
40
|
+
colorWithDeviceRed_green_blue_alpha(r,g,b,1)
|
41
|
+
elsif name == "transparent"
|
42
|
+
OSX::NSColor.blackColor.colorWithAlphaComponent(0.0)
|
43
|
+
elsif name == "grey"
|
44
|
+
OSX::NSColor.colorWithDeviceRed_green_blue_alpha(0.5,0.5,0.5,1)
|
45
|
+
else
|
46
|
+
eval("OSX::NSColor.#{name}Color")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module Magick
|
52
|
+
class Draw
|
53
|
+
attr_accessor :pointsize, :fill, :stroke, :font_weight, :gravity, :font,
|
54
|
+
:stroke_color, :fill_color, :stroke_width, :dasharray
|
55
|
+
def stroke_and_fill(path)
|
56
|
+
@stroke_color.colorWithAlphaComponent(@stroke_opacity).set
|
57
|
+
path.stroke
|
58
|
+
@fill_color.colorWithAlphaComponent(@fill_opacity).set
|
59
|
+
path.fill
|
60
|
+
end
|
61
|
+
def initialize
|
62
|
+
@fill = "white"
|
63
|
+
@pointsize = 18
|
64
|
+
@stroke_opacity = 1
|
65
|
+
@fill_opacity = 1
|
66
|
+
@stroke_width = 1
|
67
|
+
@stroke_color = OSX::NSColor.colorWithName("grey")
|
68
|
+
@fill_color = OSX::NSColor.colorWithName("grey")
|
69
|
+
@stack = []
|
70
|
+
OSX::NSBezierPath.setDefaultLineJoinStyle(OSX::NSRoundLineJoinStyle)
|
71
|
+
OSX::NSBezierPath.setDefaultLineWidth(1)
|
72
|
+
end
|
73
|
+
def push
|
74
|
+
log "Draw.push"
|
75
|
+
@stack.push({ # add more if needed
|
76
|
+
:fill_color => @fill_color,
|
77
|
+
:stroke_color => @stroke_color,
|
78
|
+
:dasharray => @dasharray,
|
79
|
+
:dashcount => @dashcount,
|
80
|
+
:stroke_width => @stroke_width
|
81
|
+
})
|
82
|
+
self
|
83
|
+
end
|
84
|
+
def pop
|
85
|
+
log "Draw.pop"
|
86
|
+
hash = @stack.pop
|
87
|
+
hash.keys.each {|key| self.instance_variable_set("@"+key.to_s, hash[key])}
|
88
|
+
self
|
89
|
+
end
|
90
|
+
def scale(x,y)
|
91
|
+
log "scale #{x} #{y}"
|
92
|
+
@scalex = x
|
93
|
+
@scaley = y
|
94
|
+
self
|
95
|
+
end
|
96
|
+
def string_attributes
|
97
|
+
attributes = OSX::NSMutableDictionary.alloc.initWithCapacity_(10)
|
98
|
+
attributes.setObject_forKey(OSX::NSColor.colorWithName(@fill),
|
99
|
+
OSX.NSForegroundColorAttributeName)
|
100
|
+
if @font_weight == BoldWeight
|
101
|
+
attributes.setObject_forKey(OSX::NSFont.boldSystemFontOfSize(@pointsize),
|
102
|
+
OSX.NSFontAttributeName)
|
103
|
+
else
|
104
|
+
attributes.setObject_forKey(OSX::NSFont.systemFontOfSize(@pointsize),
|
105
|
+
OSX.NSFontAttributeName)
|
106
|
+
end
|
107
|
+
attributes
|
108
|
+
end
|
109
|
+
def get_type_metrics(a,text)
|
110
|
+
log "get_type_metrics #{a} #{text.inspect}"
|
111
|
+
attributes = string_attributes()
|
112
|
+
metrics = TypeMetric.new
|
113
|
+
size = OSX::NSString.stringWithString(text).sizeWithAttributes(attributes)
|
114
|
+
metrics.width = size.width
|
115
|
+
metrics
|
116
|
+
end
|
117
|
+
def annotate(image,width,height,x,y,text)
|
118
|
+
log "annotate #{image} #{width} #{height} #{x} #{y} #{text}"
|
119
|
+
attributes = string_attributes()
|
120
|
+
string = OSX::NSString.stringWithString(text)
|
121
|
+
size = string.sizeWithAttributes(attributes)
|
122
|
+
if @gravity == EastGravity or @gravity == SouthEastGravity or @gravity == NorthEastGravity
|
123
|
+
x = x + width - size.width/2
|
124
|
+
elsif @gravity == CenterGravity or @gravity == NorthGravity or @gravity == SouthGravity
|
125
|
+
x = x + width/2 - size.width/2
|
126
|
+
end
|
127
|
+
if @gravity == NorthGravity or @gravity == NorthEastGravity or @gravity == NorthWestGravity
|
128
|
+
y = y + height - size.height
|
129
|
+
elsif @gravity == CenterGravity or @gravity == EastGravity or @gravity == WestGravity
|
130
|
+
y = y + height/2 - size.height/2
|
131
|
+
else
|
132
|
+
y = y - size.height/2
|
133
|
+
end
|
134
|
+
string.drawAtPoint_withAttributes([x,image.height-y-size.height], attributes)
|
135
|
+
self
|
136
|
+
end
|
137
|
+
def stroke(color)
|
138
|
+
log "stroke #{color}"
|
139
|
+
@stroke_color = OSX::NSColor.colorWithName(color)
|
140
|
+
self
|
141
|
+
end
|
142
|
+
def fill(color)
|
143
|
+
log "fill #{color}"
|
144
|
+
@fill_color = OSX::NSColor.colorWithName(color)
|
145
|
+
self
|
146
|
+
end
|
147
|
+
def rectangle(a,b,c,d)
|
148
|
+
log "rectangle #{a} #{b} #{c} #{d}"
|
149
|
+
@fill_color.set
|
150
|
+
OSX::NSRectFill([a*@scalex,$height-d*@scalex,(c-a)*@scalex,(d-b)*@scaley])
|
151
|
+
self
|
152
|
+
end
|
153
|
+
def stroke_width(a)
|
154
|
+
log "stroke_width #{a}"
|
155
|
+
@stroke_width = a
|
156
|
+
OSX::NSBezierPath.setDefaultLineWidth(a*@scalex)
|
157
|
+
self
|
158
|
+
end
|
159
|
+
def line(a,b,c,d)
|
160
|
+
log "line #{a} #{b} #{c} #{d}"
|
161
|
+
@stroke_color.set
|
162
|
+
path = OSX::NSBezierPath.bezierPath
|
163
|
+
path.setLineDash_count_phase(@dasharray, @dashcount, 0) if @dasharray
|
164
|
+
path.moveToPoint([a*@scalex,$height-b*@scaley])
|
165
|
+
path.lineToPoint([c*@scalex,$height-d*@scaley])
|
166
|
+
path.stroke
|
167
|
+
self
|
168
|
+
end
|
169
|
+
def stroke_opacity(opacity)
|
170
|
+
log "stroke_opacity #{opacity}"
|
171
|
+
@stroke_opacity = opacity
|
172
|
+
self
|
173
|
+
end
|
174
|
+
def stroke_color(color)
|
175
|
+
stroke(color)
|
176
|
+
self
|
177
|
+
end
|
178
|
+
def fill_opacity(opacity)
|
179
|
+
@fill_opacity = opacity
|
180
|
+
self
|
181
|
+
end
|
182
|
+
def stroke_dasharray(a, b)
|
183
|
+
log "stroke_dasharray #{a} #{b}"
|
184
|
+
@dashcount = 2
|
185
|
+
@dasharray = [a,b].pack('f2')
|
186
|
+
self
|
187
|
+
end
|
188
|
+
def circle(cx,cy,px,py)
|
189
|
+
log "circle #{cx} #{cy} #{px} #{py}"
|
190
|
+
r = Math::sqrt((cx-px)*(cx-px) + (cy-py)*(cy-py))
|
191
|
+
path = OSX::NSBezierPath.bezierPathWithOvalInRect(
|
192
|
+
[(cx-r)*@scalex,$height-(cy+r)*@scaley,2*r*@scalex,2*r*@scaley])
|
193
|
+
stroke_and_fill(path)
|
194
|
+
self
|
195
|
+
end
|
196
|
+
def ellipse(x, y, w, h, as, ae)
|
197
|
+
log "ellipse #{x} #{y} #{w} #{h} #{as} #{ae}"
|
198
|
+
push
|
199
|
+
stroke_width(1)
|
200
|
+
@fill_color = @stroke_color
|
201
|
+
path = OSX::NSBezierPath.bezierPath
|
202
|
+
center = [x*@scalex,$height-y*@scaley]
|
203
|
+
path.moveToPoint(center)
|
204
|
+
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle(
|
205
|
+
center,2*w*@scalex,-ae,-as)
|
206
|
+
path.closePath
|
207
|
+
stroke_and_fill(path)
|
208
|
+
pop
|
209
|
+
self
|
210
|
+
end
|
211
|
+
def polyline(*args)
|
212
|
+
path = OSX::NSBezierPath.bezierPath
|
213
|
+
path.moveToPoint([args[0]*@scalex,$height-args[1]*@scaley])
|
214
|
+
i = 2
|
215
|
+
while(i < args.length)
|
216
|
+
path.lineToPoint([args[i]*@scalex,$height-args[i+1]*@scaley])
|
217
|
+
i = i + 2
|
218
|
+
end
|
219
|
+
stroke_and_fill(path)
|
220
|
+
self
|
221
|
+
end
|
222
|
+
def polygon(*args)
|
223
|
+
path = OSX::NSBezierPath.bezierPath
|
224
|
+
path.moveToPoint([args[0]*@scalex,$height-args[1]*@scaley])
|
225
|
+
i = 2
|
226
|
+
while(i < args.length)
|
227
|
+
path.lineToPoint([args[i]*@scalex,$height-args[i+1]*@scaley])
|
228
|
+
i = i + 2
|
229
|
+
end
|
230
|
+
path.closePath
|
231
|
+
stroke_and_fill(path)
|
232
|
+
self
|
233
|
+
end
|
234
|
+
def draw(a)
|
235
|
+
log "draw #{a}"
|
236
|
+
self
|
237
|
+
end
|
238
|
+
end
|
239
|
+
class Image
|
240
|
+
attr_accessor :image, :width, :height
|
241
|
+
def initialize(width, height, fill=nil)
|
242
|
+
log "Image.initialize #{width} #{height} #{fill}"
|
243
|
+
@width = width
|
244
|
+
@height = height
|
245
|
+
@image = OSX::NSImage.alloc.initWithSize([width,height])
|
246
|
+
@image.lockFocus
|
247
|
+
if fill == nil
|
248
|
+
OSX::NSColor.blackColor.set
|
249
|
+
OSX::NSRectFill([0,0,width,height])
|
250
|
+
elsif fill.class == Magick::GradientFill
|
251
|
+
gradient = fill.image
|
252
|
+
gradient.drawInRect_fromRect_operation_fraction([0,0,width,height],
|
253
|
+
[0,0,gradient.size.width,gradient.size.height], OSX::NSCompositeCopy, 1.0)
|
254
|
+
else
|
255
|
+
fill.drawInRect_fromRect_operation_fraction([0,0,width,height],
|
256
|
+
[0,0,fill.size.width,fill.size.height], OSX::NSCompositeCopy, 1.0)
|
257
|
+
end
|
258
|
+
$width = width
|
259
|
+
$height = height
|
260
|
+
self
|
261
|
+
end
|
262
|
+
def write(filename)
|
263
|
+
log "Image.write #{filename}"
|
264
|
+
@image.unlockFocus
|
265
|
+
@image.writePNG(filename)
|
266
|
+
self
|
267
|
+
end
|
268
|
+
def self.read(filename)
|
269
|
+
log "Image.read #{filename}"
|
270
|
+
image = OSX::NSImage.alloc.initWithContentsOfFile(filename)
|
271
|
+
[Image.new(800,600,image)]
|
272
|
+
end
|
273
|
+
end
|
274
|
+
class GradientFill
|
275
|
+
attr_accessor :image
|
276
|
+
def initialize(a,b,c,d,e,f)
|
277
|
+
log "GradientFill.initialize #{a} #{b} #{c} #{d} #{e} #{f}"
|
278
|
+
# There's no direct gradient fill support in Cocoa, so fake it
|
279
|
+
resolution = 100 # increase this to reduce striping and increase run time...
|
280
|
+
@image = OSX::NSImage.alloc.initWithSize([1,resolution])
|
281
|
+
@image.lockFocus
|
282
|
+
bottomColor = OSX::NSColor.colorWithName(f)
|
283
|
+
topColor = OSX::NSColor.colorWithName(e)
|
284
|
+
resolution.times {|i|
|
285
|
+
bottomColor.blendedColorWithFraction_ofColor(i/(resolution - 1.0), topColor).set
|
286
|
+
OSX::NSRectFill([0,i,1,i+1])
|
287
|
+
}
|
288
|
+
self
|
289
|
+
end
|
290
|
+
end
|
291
|
+
class TypeMetric
|
292
|
+
attr_accessor :width
|
293
|
+
def initialize
|
294
|
+
@width = 50
|
295
|
+
end
|
296
|
+
end
|
297
|
+
NormalWeight = 400
|
298
|
+
BoldWeight = 700
|
299
|
+
WestGravity = :west
|
300
|
+
EastGravity = :east
|
301
|
+
CenterGravity = :center
|
302
|
+
SouthGravity = :south
|
303
|
+
NorthGravity = :north
|
304
|
+
SouthWestGravity = :southwest
|
305
|
+
NorthWestGravity = :northwest
|
306
|
+
NorthEastGravity = :northeast
|
307
|
+
SouthEastGravity = :southeast
|
308
|
+
end
|
309
|
+
|
310
|
+
# this prevents a warning from AppKit
|
311
|
+
app = OSX::NSApplication.sharedApplication
|
data/lib/gruff/bar.rb
CHANGED
@@ -16,11 +16,16 @@ class Gruff::Bar < Gruff::Base
|
|
16
16
|
|
17
17
|
@d = @d.stroke_opacity 0.0
|
18
18
|
|
19
|
-
#
|
19
|
+
# Setup the BarConversion Object
|
20
20
|
conversion = Gruff::BarConversion.new()
|
21
21
|
conversion.graph_height = @graph_height
|
22
22
|
conversion.graph_top = @graph_top
|
23
|
-
|
23
|
+
|
24
|
+
# Labels will be centered over the left of the bar if
|
25
|
+
# there are more labels than columns.
|
26
|
+
center_labels_left = (@labels.keys.length > @column_count ? true : false)
|
27
|
+
|
28
|
+
# Set up the right mode [1,2,3] see BarConversion for further explanation
|
24
29
|
if @minimum_value >= 0 then
|
25
30
|
# all bars go from zero to positiv
|
26
31
|
conversion.mode = 1
|
@@ -54,12 +59,18 @@ class Gruff::Bar < Gruff::Base
|
|
54
59
|
@d = @d.rectangle(left_x, conv[0], right_x, conv[1])
|
55
60
|
|
56
61
|
# Calculate center based on bar_width and current row
|
57
|
-
label_center = @graph_left +
|
58
|
-
|
62
|
+
label_center = @graph_left +
|
63
|
+
(@data.length * @bar_width * point_index) +
|
64
|
+
(@data.length * @bar_width / 2.0)
|
65
|
+
# Subtract half a bar width to center left if requested
|
66
|
+
draw_label(label_center - (center_labels_left ? @bar_width / 2.0 : 0.0), point_index)
|
59
67
|
end
|
60
68
|
|
61
69
|
end
|
62
70
|
|
71
|
+
# Draw the last label if requested
|
72
|
+
draw_label(@graph_right, @column_count) if center_labels_left
|
73
|
+
|
63
74
|
@d.draw(@base_image)
|
64
75
|
end
|
65
76
|
|
data/lib/gruff/base.rb
CHANGED
@@ -14,20 +14,33 @@
|
|
14
14
|
# and a cast of thousands.
|
15
15
|
#
|
16
16
|
|
17
|
+
require 'rubygems'
|
17
18
|
require 'RMagick'
|
19
|
+
require File.dirname(__FILE__) + '/deprecated'
|
18
20
|
|
19
21
|
module Gruff
|
20
22
|
|
21
|
-
VERSION = '0.
|
23
|
+
VERSION = '0.2.3'
|
22
24
|
|
23
25
|
class Base
|
24
26
|
|
25
27
|
include Magick
|
28
|
+
include Deprecated
|
26
29
|
|
30
|
+
# Draw extra lines showing where the margins and text centers are
|
31
|
+
DEBUG = false
|
32
|
+
|
33
|
+
# Used for navigating the array of data to plot
|
27
34
|
DATA_LABEL_INDEX = 0
|
28
35
|
DATA_VALUES_INDEX = 1
|
29
36
|
DATA_COLOR_INDEX = 2
|
30
37
|
|
38
|
+
# Blank space around the edges of the graph
|
39
|
+
TOP_MARGIN = BOTTOM_MARGIN = RIGHT_MARGIN = LEFT_MARGIN = 20.0
|
40
|
+
|
41
|
+
# Space around text elements. Mostly used for vertical spacing
|
42
|
+
LEGEND_MARGIN = TITLE_MARGIN = LABEL_MARGIN = 10.0
|
43
|
+
|
31
44
|
# A hash of names for the individual columns, where the key is the array index for the column this label represents.
|
32
45
|
#
|
33
46
|
# Not all columns need to be named.
|
@@ -35,6 +48,17 @@ module Gruff
|
|
35
48
|
# Example: 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008
|
36
49
|
attr_accessor :labels
|
37
50
|
|
51
|
+
# A label for the bottom of the graph
|
52
|
+
attr_accessor :x_axis_label
|
53
|
+
|
54
|
+
# A label for the left side of the graph
|
55
|
+
attr_accessor :y_axis_label
|
56
|
+
|
57
|
+
# attr_accessor :x_axis_increment
|
58
|
+
|
59
|
+
# Manually set increment of the horizontal marking lines
|
60
|
+
attr_accessor :y_axis_increment
|
61
|
+
|
38
62
|
# Get or set the list of colors that will be used to draw the bars or lines.
|
39
63
|
attr_accessor :colors
|
40
64
|
|
@@ -64,6 +88,9 @@ module Gruff
|
|
64
88
|
|
65
89
|
# The color of the auxiliary labels and lines
|
66
90
|
attr_accessor :marker_color
|
91
|
+
|
92
|
+
# The number of horizontal lines shown for reference
|
93
|
+
attr_accessor :marker_count
|
67
94
|
|
68
95
|
# The font size of the large title at the top of the graph
|
69
96
|
attr_accessor :title_font_size
|
@@ -105,11 +132,13 @@ module Gruff
|
|
105
132
|
@raw_columns = 800.0
|
106
133
|
@raw_rows = 800.0 * (@rows/@columns)
|
107
134
|
@column_count = 0
|
135
|
+
@marker_count = nil
|
108
136
|
@maximum_value = @minimum_value = nil
|
109
137
|
@has_data = false
|
110
138
|
@data = Array.new
|
111
139
|
@labels = Hash.new
|
112
140
|
@labels_seen = Hash.new
|
141
|
+
|
113
142
|
@scale = @columns / @raw_columns
|
114
143
|
|
115
144
|
vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
|
@@ -181,6 +210,11 @@ module Gruff
|
|
181
210
|
end
|
182
211
|
end
|
183
212
|
|
213
|
+
def font=(font_path)
|
214
|
+
@font = font_path
|
215
|
+
@d.font = @font
|
216
|
+
end
|
217
|
+
|
184
218
|
# Add a color to the list of available colors for lines.
|
185
219
|
#
|
186
220
|
# Example:
|
@@ -200,6 +234,14 @@ module Gruff
|
|
200
234
|
|
201
235
|
# A color scheme similar to the popular presentation software.
|
202
236
|
def theme_keynote
|
237
|
+
# defaults = {
|
238
|
+
# :colors => ['black', 'white'],
|
239
|
+
# :additional_line_colors => ['grey'],
|
240
|
+
# :marker_color => 'white',
|
241
|
+
# :background_colors => nil,
|
242
|
+
# :background_image => nil
|
243
|
+
# }
|
244
|
+
|
203
245
|
reset_themes()
|
204
246
|
# Colors
|
205
247
|
@blue = '#6886B4'
|
@@ -284,7 +326,8 @@ module Gruff
|
|
284
326
|
# data("Bart S.", [95, 45, 78, 89, 88, 76], '#ffcc00')
|
285
327
|
#
|
286
328
|
def data(name, data_points=[], color=nil)
|
287
|
-
|
329
|
+
data_points = Array(data_points) # make sure it's an array
|
330
|
+
@data << [name, data_points, (color || increment_color)]
|
288
331
|
# Set column count if this is larger than previous counts
|
289
332
|
@column_count = (data_points.length > @column_count) ? data_points.length : @column_count
|
290
333
|
|
@@ -298,15 +341,12 @@ module Gruff
|
|
298
341
|
end
|
299
342
|
|
300
343
|
# TODO Doesn't work with stacked bar graphs
|
301
|
-
#Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
|
344
|
+
# Original: @maximum_value = larger_than_max?(data_point, index) ? max(data_point, index) : @maximum_value
|
302
345
|
@maximum_value = larger_than_max?(data_point) ? data_point : @maximum_value
|
303
|
-
if @maximum_value > 0
|
304
|
-
|
305
|
-
end
|
346
|
+
@has_data = true if @maximum_value > 0
|
347
|
+
|
306
348
|
@minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
|
307
|
-
|
308
|
-
@has_data = true
|
309
|
-
end
|
349
|
+
@has_data = true if @minimum_value < 0
|
310
350
|
end
|
311
351
|
end
|
312
352
|
|
@@ -326,51 +366,17 @@ module Gruff
|
|
326
366
|
end
|
327
367
|
end
|
328
368
|
|
329
|
-
def scale_measurements
|
330
|
-
setup_graph_measurements
|
331
|
-
end
|
332
|
-
|
333
|
-
def total_height
|
334
|
-
@rows + 10
|
335
|
-
end
|
336
|
-
|
337
|
-
def graph_top
|
338
|
-
@graph_top * @scale
|
339
|
-
end
|
340
|
-
|
341
|
-
def graph_height
|
342
|
-
@graph_height * @scale
|
343
|
-
end
|
344
|
-
|
345
|
-
def graph_left
|
346
|
-
@graph_left * @scale
|
347
|
-
end
|
348
|
-
|
349
|
-
def graph_width
|
350
|
-
@graph_width * @scale
|
351
|
-
end
|
352
|
-
|
353
|
-
|
354
369
|
protected
|
355
370
|
|
356
|
-
|
357
|
-
def make_stacked
|
358
|
-
stacked_values = Array.new(@column_count, 0)
|
359
|
-
@data.each do |value_set|
|
360
|
-
value_set[1].each_with_index do |value, index|
|
361
|
-
stacked_values[index] += value
|
362
|
-
end
|
363
|
-
value_set[1] = stacked_values.dup
|
364
|
-
end
|
365
|
-
end
|
366
|
-
|
367
|
-
|
368
371
|
# Overridden by subclasses to do the actual plotting of the graph.
|
369
372
|
#
|
370
373
|
# Subclasses should start by calling super() for this method.
|
371
374
|
def draw
|
372
375
|
make_stacked if @stacked
|
373
376
|
setup_drawing()
|
377
|
+
|
378
|
+
debug { @d.rectangle( LEFT_MARGIN, TOP_MARGIN,
|
379
|
+
@raw_columns - RIGHT_MARGIN, @raw_rows - BOTTOM_MARGIN) }
|
374
380
|
|
375
381
|
# Subclasses will do some drawing here...
|
376
382
|
#@d.draw(@base_image)
|
@@ -392,26 +398,26 @@ protected
|
|
392
398
|
sort_norm_data() # Sort norm_data with avg largest values set first (for display)
|
393
399
|
|
394
400
|
draw_legend()
|
395
|
-
setup_graph_height()
|
396
401
|
draw_line_markers()
|
402
|
+
draw_axis_labels()
|
397
403
|
draw_title
|
398
404
|
end
|
399
405
|
|
400
406
|
# Make copy of data with values scaled between 0-100
|
401
|
-
def normalize
|
402
|
-
if @norm_data.nil?
|
407
|
+
def normalize(force=false)
|
408
|
+
if @norm_data.nil? || force
|
403
409
|
@norm_data = []
|
404
410
|
return unless @has_data
|
405
|
-
|
406
|
-
|
407
|
-
|
411
|
+
|
412
|
+
calculate_spread
|
413
|
+
|
408
414
|
@data.each do |data_row|
|
409
415
|
norm_data_points = []
|
410
416
|
data_row[DATA_VALUES_INDEX].each do |data_point|
|
411
417
|
if data_point.nil?
|
412
418
|
norm_data_points << nil
|
413
419
|
else
|
414
|
-
norm_data_points << ((data_point.to_f -
|
420
|
+
norm_data_points << ((data_point.to_f - @minimum_value.to_f ) / @spread)
|
415
421
|
end
|
416
422
|
end
|
417
423
|
@norm_data << [data_row[DATA_LABEL_INDEX], norm_data_points, data_row[DATA_COLOR_INDEX]]
|
@@ -419,28 +425,80 @@ protected
|
|
419
425
|
end
|
420
426
|
end
|
421
427
|
|
422
|
-
def
|
423
|
-
@
|
428
|
+
def calculate_spread
|
429
|
+
@spread = @maximum_value.to_f - @minimum_value.to_f
|
430
|
+
@spread = @spread > 0 ? @spread : 1
|
424
431
|
end
|
425
432
|
|
426
433
|
def setup_graph_measurements
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
@
|
434
|
+
@marker_caps_height = calculate_caps_height(@marker_font_size)
|
435
|
+
@title_caps_height = calculate_caps_height(@title_font_size)
|
436
|
+
@legend_caps_height = calculate_caps_height(@legend_font_size)
|
437
|
+
|
438
|
+
if @hide_line_markers
|
439
|
+
(@graph_left,
|
440
|
+
@graph_right_margin,
|
441
|
+
@graph_bottom_margin) = [LEFT_MARGIN, RIGHT_MARGIN, BOTTOM_MARGIN]
|
433
442
|
else
|
434
|
-
@graph_left =
|
443
|
+
@graph_left = LEFT_MARGIN +
|
444
|
+
calculate_width(@marker_font_size, label(@maximum_value.to_f)) +
|
445
|
+
LABEL_MARGIN * 2 +
|
446
|
+
(@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
|
447
|
+
# Make space for half the width of the rightmost column label.
|
448
|
+
# Might be greater than the number of columns if between-style bar markers are used.
|
449
|
+
last_label = @labels.keys.sort.last.to_i
|
450
|
+
@graph_right_margin = RIGHT_MARGIN +
|
451
|
+
(last_label >= (@column_count-1) ? calculate_width(@marker_font_size, @labels[last_label])/2.0 : 0)
|
452
|
+
@graph_bottom_margin = BOTTOM_MARGIN +
|
453
|
+
@marker_caps_height + LABEL_MARGIN
|
435
454
|
end
|
436
455
|
|
437
456
|
@graph_right = @raw_columns - @graph_right_margin
|
438
|
-
|
439
457
|
@graph_width = @raw_columns - @graph_left - @graph_right_margin
|
440
458
|
|
441
|
-
@
|
442
|
-
|
443
|
-
|
459
|
+
# When @hide title, leave a TITLE_MARGIN space for aesthetics.
|
460
|
+
# Same with @hide_legend
|
461
|
+
@graph_top = TOP_MARGIN +
|
462
|
+
(@hide_title ? TITLE_MARGIN : @title_caps_height + TITLE_MARGIN * 2) +
|
463
|
+
(@hide_legend ? LEGEND_MARGIN : @legend_caps_height + LEGEND_MARGIN * 2)
|
464
|
+
|
465
|
+
@graph_bottom = @raw_rows - @graph_bottom_margin -
|
466
|
+
(@x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN)
|
467
|
+
|
468
|
+
@graph_height = @graph_bottom - @graph_top
|
469
|
+
end
|
470
|
+
|
471
|
+
# Draw the optional labels for the x axis and y axis.
|
472
|
+
def draw_axis_labels
|
473
|
+
unless @x_axis_label.nil?
|
474
|
+
# X Axis
|
475
|
+
# Centered vertically and horizontally by setting the
|
476
|
+
# height to 1.0 and the width to the width of the graph.
|
477
|
+
x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN * 2 + @marker_caps_height
|
478
|
+
|
479
|
+
# TODO Center between graph area
|
480
|
+
@d.fill = @marker_color
|
481
|
+
@d.font = @font if @font
|
482
|
+
@d.stroke = 'transparent'
|
483
|
+
@d.pointsize = scale_fontsize(@marker_font_size)
|
484
|
+
@d.gravity = NorthGravity
|
485
|
+
@d = @d.annotate_scaled( @base_image,
|
486
|
+
@raw_columns, 1.0,
|
487
|
+
0.0, x_axis_label_y_coordinate,
|
488
|
+
@x_axis_label, @scale)
|
489
|
+
debug { @d.line 0.0, x_axis_label_y_coordinate, @raw_columns, x_axis_label_y_coordinate }
|
490
|
+
end
|
491
|
+
|
492
|
+
unless @y_axis_label.nil?
|
493
|
+
# Y Axis, rotated vertically
|
494
|
+
@d.rotation = 90.0
|
495
|
+
@d.gravity = CenterGravity
|
496
|
+
@d = @d.annotate_scaled( @base_image,
|
497
|
+
1.0, @raw_rows,
|
498
|
+
LEFT_MARGIN + @marker_caps_height / 2.0, 0.0,
|
499
|
+
@y_axis_label, @scale)
|
500
|
+
@d.rotation = -90.0
|
501
|
+
end
|
444
502
|
end
|
445
503
|
|
446
504
|
# Draws horizontal background lines and labels
|
@@ -449,20 +507,39 @@ protected
|
|
449
507
|
|
450
508
|
# Draw horizontal line markers and annotate with numbers
|
451
509
|
@d = @d.stroke(@marker_color)
|
452
|
-
@d = @d.stroke_width 1
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
510
|
+
@d = @d.stroke_width 1
|
511
|
+
|
512
|
+
if @y_axis_increment.nil?
|
513
|
+
# Try to use a number of horizontal lines that will come out even.
|
514
|
+
#
|
515
|
+
# TODO Do the same for larger numbers...100, 75, 50, 25
|
516
|
+
if @marker_count.nil?
|
517
|
+
(3..7).each do |lines|
|
518
|
+
if @spread % lines == 0.0
|
519
|
+
@marker_count = lines
|
520
|
+
break
|
521
|
+
end
|
522
|
+
end
|
523
|
+
@marker_count ||= 4
|
524
|
+
end
|
525
|
+
@increment = (@spread > 0) ? significant(@spread / @marker_count) : 1
|
526
|
+
else
|
527
|
+
# TODO Make this work for negative values
|
528
|
+
@maximum_value = [@maximum_value.ceil, @y_axis_increment].max
|
529
|
+
@minimum_value = @minimum_value.floor
|
530
|
+
calculate_spread
|
531
|
+
normalize(true)
|
532
|
+
|
533
|
+
@marker_count = (@spread / @y_axis_increment).to_i
|
534
|
+
@increment = @y_axis_increment
|
535
|
+
end
|
536
|
+
@increment_scaled = @graph_height.to_f / (@spread / @increment)
|
460
537
|
|
461
|
-
(0
|
462
|
-
y = @graph_top + @graph_height - index.to_f *
|
538
|
+
(0..@marker_count).each do |index|
|
539
|
+
y = @graph_top + @graph_height - index.to_f * @increment_scaled
|
463
540
|
@d = @d.line(@graph_left, y, @graph_right, y)
|
464
541
|
|
465
|
-
marker_label = index * increment + @minimum_value.to_f
|
542
|
+
marker_label = index * @increment + @minimum_value.to_f
|
466
543
|
|
467
544
|
@d.fill = @marker_color
|
468
545
|
@d.font = @font if @font
|
@@ -470,16 +547,19 @@ protected
|
|
470
547
|
@d.pointsize = scale_fontsize(@marker_font_size)
|
471
548
|
@d.gravity = EastGravity
|
472
549
|
|
550
|
+
# Vertically center with 1.0 for the height
|
473
551
|
@d = @d.annotate_scaled( @base_image,
|
474
|
-
|
475
|
-
|
476
|
-
marker_label
|
552
|
+
@graph_left - LABEL_MARGIN, 1.0,
|
553
|
+
0.0, y,
|
554
|
+
label(marker_label), @scale)
|
477
555
|
end
|
556
|
+
|
557
|
+
# Submitted by a contibutor...the utility escapes me
|
478
558
|
i = 0
|
479
559
|
@additional_line_values.each do |value|
|
480
|
-
|
560
|
+
@increment_scaled = @graph_height.to_f / (@maximum_value.to_f / value)
|
481
561
|
|
482
|
-
y = @graph_top + @graph_height -
|
562
|
+
y = @graph_top + @graph_height - @increment_scaled
|
483
563
|
|
484
564
|
@d = @d.stroke(@additional_line_colors[i])
|
485
565
|
@d = @d.line(@graph_left, y, @graph_right, y)
|
@@ -498,13 +578,15 @@ protected
|
|
498
578
|
end
|
499
579
|
end
|
500
580
|
|
501
|
-
# Draws a legend with the names of the datasets
|
581
|
+
# Draws a legend with the names of the datasets
|
582
|
+
# matched to the colors used to draw them.
|
502
583
|
def draw_legend
|
503
584
|
return if @hide_legend
|
504
585
|
|
505
586
|
@legend_labels = @data.collect {|item| item[DATA_LABEL_INDEX] }
|
506
587
|
|
507
588
|
legend_square_width = 20 # small square with color of this item
|
589
|
+
legend_square_margin = 4
|
508
590
|
|
509
591
|
# May fix legend drawing problem at small sizes
|
510
592
|
@d.font = @font if @font
|
@@ -512,12 +594,22 @@ protected
|
|
512
594
|
|
513
595
|
metrics = @d.get_type_metrics(@base_image, @legend_labels.join(''))
|
514
596
|
legend_text_width = metrics.width
|
515
|
-
legend_width = legend_text_width +
|
597
|
+
legend_width = legend_text_width +
|
598
|
+
(@legend_labels.length * legend_square_width * 2.7)
|
516
599
|
legend_left = (@raw_columns - legend_width) / 2
|
517
600
|
legend_increment = legend_width / @legend_labels.length.to_f
|
518
601
|
|
519
602
|
current_x_offset = legend_left
|
603
|
+
current_y_offset = @hide_title ?
|
604
|
+
TOP_MARGIN + LEGEND_MARGIN :
|
605
|
+
TOP_MARGIN +
|
606
|
+
TITLE_MARGIN + @title_caps_height +
|
607
|
+
LEGEND_MARGIN
|
608
|
+
|
609
|
+
debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
|
610
|
+
|
520
611
|
@legend_labels.each_with_index do |legend_label, index|
|
612
|
+
|
521
613
|
# Draw label
|
522
614
|
@d.fill = @marker_color
|
523
615
|
@d.font = @font if @font
|
@@ -526,16 +618,17 @@ protected
|
|
526
618
|
@d.font_weight = NormalWeight
|
527
619
|
@d.gravity = WestGravity
|
528
620
|
@d = @d.annotate_scaled( @base_image,
|
529
|
-
@raw_columns,
|
530
|
-
current_x_offset + (legend_square_width * 1.7),
|
621
|
+
@raw_columns, 1.0,
|
622
|
+
current_x_offset + (legend_square_width * 1.7), current_y_offset,
|
531
623
|
legend_label.to_s, @scale)
|
532
624
|
|
533
625
|
# Now draw box with color of this dataset
|
534
|
-
legend_box_y_offset = 2 # Move box down slightly to center
|
535
626
|
@d = @d.stroke 'transparent'
|
536
627
|
@d = @d.fill @data[index][DATA_COLOR_INDEX]
|
537
|
-
@d = @d.rectangle(current_x_offset,
|
538
|
-
|
628
|
+
@d = @d.rectangle(current_x_offset,
|
629
|
+
current_y_offset - legend_square_width / 2.0,
|
630
|
+
current_x_offset + legend_square_width,
|
631
|
+
current_y_offset + legend_square_width / 2.0)
|
539
632
|
|
540
633
|
@d.pointsize = @legend_font_size
|
541
634
|
metrics = @d.get_type_metrics(@base_image, legend_label.to_s)
|
@@ -553,30 +646,36 @@ protected
|
|
553
646
|
@d.stroke = 'transparent'
|
554
647
|
@d.pointsize = scale_fontsize(@title_font_size)
|
555
648
|
@d.font_weight = BoldWeight
|
556
|
-
@d.gravity =
|
649
|
+
@d.gravity = NorthGravity
|
557
650
|
@d = @d.annotate_scaled( @base_image,
|
558
|
-
@raw_columns,
|
559
|
-
0,
|
651
|
+
@raw_columns, 1.0,
|
652
|
+
0, TOP_MARGIN,
|
560
653
|
@title, @scale)
|
561
654
|
end
|
562
655
|
|
563
656
|
##
|
564
657
|
# Draws column labels below graph, centered over x_offset
|
658
|
+
#
|
659
|
+
# TODO Allow WestGravity as an option
|
660
|
+
|
565
661
|
def draw_label(x_offset, index)
|
566
662
|
return if @hide_line_markers
|
567
663
|
|
568
664
|
if !@labels[index].nil? && @labels_seen[index].nil?
|
665
|
+
y_offset = @graph_bottom + LABEL_MARGIN
|
666
|
+
|
569
667
|
@d.fill = @marker_color
|
570
668
|
@d.font = @font if @font
|
571
669
|
@d.stroke = 'transparent'
|
572
670
|
@d.font_weight = NormalWeight
|
573
671
|
@d.pointsize = scale_fontsize(@marker_font_size)
|
574
|
-
@d.gravity =
|
672
|
+
@d.gravity = NorthGravity
|
575
673
|
@d = @d.annotate_scaled(@base_image,
|
576
|
-
1, 1,
|
577
|
-
x_offset,
|
674
|
+
1.0, 1.0,
|
675
|
+
x_offset, y_offset,
|
578
676
|
@labels[index], @scale)
|
579
677
|
@labels_seen[index] = 1
|
678
|
+
debug { @d.line 0.0, y_offset, @raw_columns, y_offset }
|
580
679
|
end
|
581
680
|
end
|
582
681
|
|
@@ -621,7 +720,6 @@ protected
|
|
621
720
|
|
622
721
|
@d = Draw.new
|
623
722
|
# Scale down from 800x600 used to calculate drawing.
|
624
|
-
# NOTE: Font annotation is now affected and has to be done manually.
|
625
723
|
@d = @d.scale(@scale, @scale)
|
626
724
|
end
|
627
725
|
|
@@ -629,9 +727,10 @@ protected
|
|
629
727
|
value * @scale
|
630
728
|
end
|
631
729
|
|
730
|
+
# Return a comparable fontsize for the current graph.
|
632
731
|
def scale_fontsize(value)
|
633
732
|
new_fontsize = value * @scale
|
634
|
-
#return 10
|
733
|
+
# return new_fontsize < 10.0 ? 10.0 : new_fontsize
|
635
734
|
return new_fontsize
|
636
735
|
end
|
637
736
|
|
@@ -648,10 +747,14 @@ protected
|
|
648
747
|
data_point < @minimum_value
|
649
748
|
end
|
650
749
|
|
750
|
+
##
|
751
|
+
# Overridden by subclasses that need it.
|
651
752
|
def max(data_point, index)
|
652
753
|
data_point
|
653
754
|
end
|
654
755
|
|
756
|
+
##
|
757
|
+
# Overridden by subclasses that need it.
|
655
758
|
def min(data_point, index)
|
656
759
|
data_point
|
657
760
|
end
|
@@ -706,9 +809,30 @@ protected
|
|
706
809
|
@minimum_value = 0
|
707
810
|
end
|
708
811
|
|
812
|
+
def make_stacked
|
813
|
+
stacked_values = Array.new(@column_count, 0)
|
814
|
+
@data.each do |value_set|
|
815
|
+
value_set[1].each_with_index do |value, index|
|
816
|
+
stacked_values[index] += value
|
817
|
+
end
|
818
|
+
value_set[1] = stacked_values.dup
|
819
|
+
end
|
820
|
+
end
|
709
821
|
|
710
822
|
private
|
711
823
|
|
824
|
+
# Takes a block and draws it if DEBUG is true.
|
825
|
+
#
|
826
|
+
# debug { @d.rectangle x1, y1, x2, y2 }
|
827
|
+
#
|
828
|
+
def debug
|
829
|
+
if DEBUG
|
830
|
+
@d = @d.fill 'transparent'
|
831
|
+
@d = @d.stroke 'turquoise'
|
832
|
+
@d = yield
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
712
836
|
def increment_color
|
713
837
|
if @color_index == 0
|
714
838
|
@color_index += 1
|
@@ -726,14 +850,51 @@ private
|
|
726
850
|
end
|
727
851
|
end
|
728
852
|
|
729
|
-
|
853
|
+
def label(value)
|
854
|
+
if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?
|
855
|
+
return value.to_i.to_s
|
856
|
+
end
|
857
|
+
|
858
|
+
if @spread > 10.0
|
859
|
+
sprintf("%0i", value)
|
860
|
+
elsif @spread >= 3.0
|
861
|
+
sprintf("%0.2f", value)
|
862
|
+
else
|
863
|
+
value.to_s
|
864
|
+
end
|
865
|
+
end
|
866
|
+
|
867
|
+
##
|
868
|
+
# Returns the height of the capital letter 'X' for the current font and size.
|
869
|
+
#
|
870
|
+
# Not scaled since it deals with dimensions that the regular
|
871
|
+
# scaling will handle.
|
872
|
+
|
873
|
+
def calculate_caps_height(font_size)
|
874
|
+
@d.pointsize = font_size
|
875
|
+
@d.get_type_metrics(@base_image, 'X').height
|
876
|
+
end
|
877
|
+
|
878
|
+
##
|
879
|
+
# Returns the width of a string at this pointsize.
|
880
|
+
#
|
881
|
+
# Not scaled since it deals with dimensions that the regular
|
882
|
+
# scaling will handle.
|
883
|
+
|
884
|
+
def calculate_width(font_size, text)
|
885
|
+
@d.pointsize = font_size
|
886
|
+
@d.get_type_metrics(@base_image, text.to_s).width
|
887
|
+
end
|
888
|
+
|
889
|
+
end # Gruff::Base
|
730
890
|
|
731
891
|
class ColorlistExhaustedException < StandardError; end
|
732
892
|
|
733
|
-
end
|
893
|
+
end # Gruff
|
734
894
|
|
735
895
|
|
736
896
|
module Magick
|
897
|
+
|
737
898
|
class Draw
|
738
899
|
|
739
900
|
# Additional method since Draw.scale doesn't affect annotations.
|
@@ -748,4 +909,6 @@ module Magick
|
|
748
909
|
end
|
749
910
|
|
750
911
|
end
|
751
|
-
|
912
|
+
|
913
|
+
end # Magick
|
914
|
+
|