gruff 0.1.2 → 0.2.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.
- 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
|
+
|