gruff 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,11 +1,19 @@
1
1
  CHANGELOG
2
2
 
3
+ == 0.1.2
4
+
5
+ * minimum_value and maximum_value can be set after data() to manually scale the graph
6
+ * Fixed infinite loop bug when values are all equal
7
+ * Added experimental net and spider graphs
8
+ * Added non-linear scene graph for a simple interface to complex layered graphs
9
+ * Initial refactoring of tests
10
+ * A host of other bug fixes
11
+
3
12
  == 0.0.8
4
13
 
5
14
  * NEW Sidestacked Bar Graphs. [Alun Eyre]
6
15
  * baseline_value larger than data will now show correctly. [Mike Perham]
7
16
  * hide_dots and hide_lines are now options for line graphs.
8
- *
9
17
 
10
18
  == 0.0.6
11
19
 
data/Rakefile CHANGED
@@ -28,6 +28,15 @@ task :clean do
28
28
  rm_rf 'doc'
29
29
  end
30
30
 
31
+ desc "Copy documentation to topfunky.com"
32
+ task :rdoc_deploy => [:rdoc] do
33
+ dirs = %w{doc}
34
+ onserver = "topfunky@topfunky.com:/home/topfunky/topfunky.com/clients/rails/gruff"
35
+ dirs.each do | dir|
36
+ `rsync -avz -e ssh "#{`pwd`.chomp}/#{dir}" "#{onserver}" --exclude ".svn"`
37
+ end
38
+ end
39
+
31
40
  # Run the unit tests
32
41
  Rake::TestTask.new { |t|
33
42
  t.libs << "test"
@@ -35,7 +44,6 @@ Rake::TestTask.new { |t|
35
44
  t.verbose = true
36
45
  }
37
46
 
38
-
39
47
  # Genereate the RDoc documentation
40
48
  Rake::RDocTask.new { |rdoc|
41
49
  rdoc.rdoc_dir = 'doc'
@@ -197,3 +205,5 @@ task :release => [:package] do
197
205
  end
198
206
  end
199
207
  end
208
+
209
+
@@ -6,6 +6,10 @@ require File.dirname(__FILE__) + '/gruff/area'
6
6
  require File.dirname(__FILE__) + '/gruff/bar'
7
7
  require File.dirname(__FILE__) + '/gruff/line'
8
8
  require File.dirname(__FILE__) + '/gruff/pie'
9
+ require File.dirname(__FILE__) + '/gruff/spider'
10
+ require File.dirname(__FILE__) + '/gruff/net'
9
11
  require File.dirname(__FILE__) + '/gruff/stacked_bar'
10
12
  require File.dirname(__FILE__) + '/gruff/side_stacked_bar'
11
13
  require File.dirname(__FILE__) + '/gruff/photo_bar'
14
+
15
+ require File.dirname(__FILE__) + '/gruff/scene'
@@ -6,7 +6,6 @@ class Gruff::Bar < Gruff::Base
6
6
 
7
7
  def draw
8
8
  super
9
-
10
9
  return unless @has_data
11
10
 
12
11
  # Setup spacing.
@@ -9,7 +9,8 @@
9
9
  # and also contributions by
10
10
  # Jarkko Laine, Mike Perham, Andreas Schwarz,
11
11
  # Alun Eyre, Guillaume Theoret, David Stokar,
12
- # Paul Rogers, Dave Woodward, Frank Oxener,
12
+ # Paul Rogers, Dave Woodward, Frank Oxener,
13
+ # Kevin Clark, Cies Breijs, Richard Cowin,
13
14
  # and a cast of thousands.
14
15
  #
15
16
 
@@ -17,7 +18,7 @@ require 'RMagick'
17
18
 
18
19
  module Gruff
19
20
 
20
- VERSION = '0.1.1'
21
+ VERSION = '0.1.2'
21
22
 
22
23
  class Base
23
24
 
@@ -58,10 +59,32 @@ module Gruff
58
59
  # Will be scaled down if graph is smaller than 800px wide.
59
60
  attr_accessor :legend_font_size
60
61
 
62
+ # The font size of the labels around the graph
61
63
  attr_accessor :marker_font_size
62
64
 
65
+ # The color of the auxiliary labels and lines
66
+ attr_accessor :marker_color
67
+
68
+ # The font size of the large title at the top of the graph
63
69
  attr_accessor :title_font_size
64
70
 
71
+ # You can manually set a minimum value instead of having the values guessed for you.
72
+ #
73
+ # Set it after you have given all your data to the graph object.
74
+ attr_accessor :minimum_value
75
+
76
+ # You can manually set a maximum value, such as a percentage-based graph that always goes to 100.
77
+ #
78
+ # If you use this, you must set it after you have given all your data to the graph object.
79
+ attr_accessor :maximum_value
80
+
81
+ # Experimental
82
+ attr_accessor :additional_line_values
83
+
84
+ # Experimental
85
+ attr_accessor :stacked
86
+
87
+
65
88
  # If one numerical argument is given, the graph is drawn at 4/3 ratio according to the given width (800 results in 800x600, 400 gives 400x300, etc.).
66
89
  #
67
90
  # Or, send a geometry string for other ratios ('800x400', '400x225').
@@ -100,6 +123,9 @@ module Gruff
100
123
 
101
124
  @hide_line_markers = @hide_legend = @hide_title = false
102
125
 
126
+ @additional_line_values = []
127
+ @additional_line_colors = []
128
+
103
129
  reset_themes()
104
130
  theme_keynote()
105
131
  end
@@ -121,6 +147,57 @@ module Gruff
121
147
  @colors = color_list
122
148
  end
123
149
 
150
+ # You can set a theme manually. Assign a hash to this method before you send your data.
151
+ #
152
+ # graph.theme = {
153
+ # :colors => %w(orange purple green white red),
154
+ # :marker_color => 'blue',
155
+ # :background_colors => %w(black grey)
156
+ # }
157
+ #
158
+ # :background_image => 'squirrel.png' is also possible.
159
+ #
160
+ # (Or hopefully something better looking than that.)
161
+ #
162
+ def theme=(options)
163
+ defaults = {
164
+ :colors => ['black', 'white'],
165
+ :additional_line_colors => ['grey'],
166
+ :marker_color => 'white',
167
+ :background_colors => nil,
168
+ :background_image => nil
169
+ }
170
+ options = defaults.merge options
171
+
172
+ reset_themes()
173
+
174
+ @colors = options[:colors]
175
+ @marker_color = options[:marker_color]
176
+ @additional_line_colors = options[:additional_line_colors]
177
+ if not options[:background_colors].nil?
178
+ @base_image = render_gradiated_background *options[:background_colors]
179
+ else
180
+ @base_image = render_image_background *options[:background_image]
181
+ end
182
+ end
183
+
184
+ # Add a color to the list of available colors for lines.
185
+ #
186
+ # Example:
187
+ # add_color('#c0e9d3')
188
+ def add_color(colorname)
189
+ @colors << colorname
190
+ end
191
+
192
+ # Replace the entire color list with a new array of colors. You need to have one more color
193
+ # than the number of datasets you intend to draw. Also aliased as the colors= setter method.
194
+ #
195
+ # Example:
196
+ # replace_colors('#cc99cc', '#d9e043', '#34d8a2')
197
+ def replace_colors(color_list=[])
198
+ @colors = color_list
199
+ end
200
+
124
201
  # A color scheme similar to the popular presentation software.
125
202
  def theme_keynote
126
203
  reset_themes()
@@ -228,7 +305,7 @@ module Gruff
228
305
  end
229
306
  @minimum_value = less_than_min?(data_point) ? data_point : @minimum_value
230
307
  if @minimum_value < 0
231
- @has_data = true
308
+ @has_data = true
232
309
  end
233
310
  end
234
311
  end
@@ -249,12 +326,50 @@ module Gruff
249
326
  end
250
327
  end
251
328
 
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
+
252
354
  protected
253
355
 
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
+
254
368
  # Overridden by subclasses to do the actual plotting of the graph.
255
369
  #
256
370
  # Subclasses should start by calling super() for this method.
257
371
  def draw
372
+ make_stacked if @stacked
258
373
  setup_drawing()
259
374
 
260
375
  # Subclasses will do some drawing here...
@@ -276,8 +391,9 @@ protected
276
391
  setup_graph_measurements()
277
392
  sort_norm_data() # Sort norm_data with avg largest values set first (for display)
278
393
 
279
- draw_line_markers()
280
394
  draw_legend()
395
+ setup_graph_height()
396
+ draw_line_markers()
281
397
  draw_title
282
398
  end
283
399
 
@@ -287,7 +403,7 @@ protected
287
403
  @norm_data = []
288
404
  return unless @has_data
289
405
  @spread = @maximum_value.to_f - @minimum_value.to_f
290
- @spread = 1 if @spread == 0 # Protect from divide by 0
406
+ @spread = 20.0 if @spread == 0.0 # Protect from divide by zero
291
407
  min_val = @minimum_value.to_f
292
408
  @data.each do |data_row|
293
409
  norm_data_points = []
@@ -303,6 +419,10 @@ protected
303
419
  end
304
420
  end
305
421
 
422
+ def setup_graph_height
423
+ @graph_height = @graph_bottom - @graph_top
424
+ end
425
+
306
426
  def setup_graph_measurements
307
427
  # TODO Separate horizontal lines from line number labels so they can be shown or hidden independently
308
428
  # TODO Get width of longest left-hand vertical text label and space left margin accordingly
@@ -320,7 +440,7 @@ protected
320
440
 
321
441
  @graph_top = 150.0
322
442
  @graph_bottom = @raw_rows - @graph_bottom_margin
323
- @graph_height = @graph_bottom - @graph_top
443
+ setup_graph_height()
324
444
  end
325
445
 
326
446
  # Draws horizontal background lines and labels
@@ -333,12 +453,10 @@ protected
333
453
  number_of_lines = 4
334
454
 
335
455
  # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
336
-
337
- # added relative height
338
456
  spread = @maximum_value.to_f - @minimum_value.to_f
339
- increment = significant(spread / number_of_lines)
457
+ spread = spread > 0 ? spread : 1
458
+ increment = (spread > 0) ? significant(spread / number_of_lines) : 1
340
459
  inc_graph = @graph_height.to_f / (spread / increment)
341
- #inc_graph = @graph_height.to_f / increment
342
460
 
343
461
  (0..number_of_lines).each do |index|
344
462
  y = @graph_top + @graph_height - index.to_f * inc_graph
@@ -351,11 +469,32 @@ protected
351
469
  @d.stroke = 'transparent'
352
470
  @d.pointsize = scale_fontsize(@marker_font_size)
353
471
  @d.gravity = EastGravity
472
+
354
473
  @d = @d.annotate_scaled( @base_image,
355
474
  100, 20,
356
475
  -10, y - (@marker_font_size/2.0),
357
476
  marker_label.to_s, @scale)
358
- # "help", @scale)
477
+ end
478
+ i = 0
479
+ @additional_line_values.each do |value|
480
+ inc_graph = @graph_height.to_f / (@maximum_value.to_f / value)
481
+
482
+ y = @graph_top + @graph_height - inc_graph
483
+
484
+ @d = @d.stroke(@additional_line_colors[i])
485
+ @d = @d.line(@graph_left, y, @graph_right, y)
486
+
487
+
488
+ @d.fill = @additional_line_colors[i]
489
+ @d.font = @font if @font
490
+ @d.stroke = 'transparent'
491
+ @d.pointsize = scale_fontsize(@marker_font_size)
492
+ @d.gravity = EastGravity
493
+ @d = @d.annotate_scaled( @base_image,
494
+ 100, 20,
495
+ -10, y - (@marker_font_size/2.0),
496
+ "", @scale)
497
+ i += 1
359
498
  end
360
499
  end
361
500
 
@@ -468,6 +607,13 @@ protected
468
607
  end
469
608
  image[0]
470
609
  end
610
+
611
+ # Use with a theme to make a transparent background
612
+ def render_transparent_background
613
+ Image.new(@columns, @rows) do
614
+ self.background_color = 'transparent'
615
+ end
616
+ end
471
617
 
472
618
  def reset_themes
473
619
  @color_index = 0
@@ -511,6 +657,7 @@ protected
511
657
  end
512
658
 
513
659
  def significant(inc)
660
+ return 1.0 if inc == 0 # Keep from going into infinite loop
514
661
  factor = 1.0
515
662
  while (inc < 10)
516
663
  inc *= 10
@@ -571,7 +718,10 @@ private
571
718
  @color_index += 1
572
719
  return @colors[@color_index - 1]
573
720
  else
574
- raise(ColorlistExhaustedException, "There are no more colors left to use.")
721
+ # Start over
722
+ @color_index = 0
723
+ return @colors[-1]
724
+ #raise(ColorlistExhaustedException, "There are no more colors left to use.")
575
725
  end
576
726
  end
577
727
  end
@@ -29,8 +29,7 @@ class Gruff::Line < Gruff::Base
29
29
  super args.shift
30
30
  end
31
31
 
32
- @hide_dots = false
33
- @hide_lines = args.empty? ? false : !args.shift # For backwards compatibility. Use g.hide_lines = true from now on.
32
+ @hide_dots = @hide_lines = false
34
33
  @baseline_color = 'red'
35
34
  end
36
35
 
@@ -57,7 +56,7 @@ class Gruff::Line < Gruff::Base
57
56
  end
58
57
 
59
58
  @norm_data.each do |data_row|
60
- prev_x = prev_y = 0.0
59
+ prev_x = prev_y = nil
61
60
  @d = @d.stroke data_row[DATA_COLOR_INDEX]
62
61
  @d = @d.fill data_row[DATA_COLOR_INDEX]
63
62
 
@@ -69,7 +68,7 @@ class Gruff::Line < Gruff::Base
69
68
 
70
69
  new_y = @graph_top + (@graph_height - data_point * @graph_height)
71
70
 
72
- if !@hide_lines and prev_x > 0 and prev_y > 0 then
71
+ if !@hide_lines and !prev_x.nil? and !prev_y.nil? then
73
72
  @d = @d.line(prev_x, prev_y, new_x, new_y)
74
73
  end
75
74
  @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
@@ -0,0 +1,133 @@
1
+
2
+ require File.dirname(__FILE__) + '/base'
3
+
4
+ # Experimental!!! See also the Spider graph.
5
+ class Gruff::Net < Gruff::Base
6
+
7
+ def draw
8
+
9
+ super
10
+
11
+ return unless @has_data
12
+
13
+ @radius = @graph_height / 2.0
14
+ @center_x = @graph_left + (@graph_width / 2.0)
15
+ @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
16
+
17
+ @x_increment = @graph_width / (@column_count - 1).to_f
18
+ circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
19
+
20
+ @d = @d.stroke_opacity 1.0
21
+ @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
22
+
23
+ if (defined?(@norm_baseline)) then
24
+ level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
25
+ @d = @d.push
26
+ @d.stroke_color @baseline_color
27
+ @d.fill_opacity 0.0
28
+ @d.stroke_dasharray(10, 20)
29
+ @d.stroke_width 5
30
+ @d.line(@graph_left, level, @graph_left + @graph_width, level)
31
+ @d = @d.pop
32
+ end
33
+
34
+ @norm_data.each do |data_row|
35
+ prev_x = prev_y = nil
36
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
37
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
38
+
39
+ data_row[1].each_with_index do |data_point, index|
40
+ next if data_point.nil?
41
+
42
+ rad_pos = index * Math::PI * 2 / @column_count
43
+ point_distance = data_point * @radius
44
+ start_x = @center_x + Math::sin(rad_pos) * point_distance
45
+ start_y = @center_y - Math::cos(rad_pos) * point_distance
46
+
47
+ next_index = index + 1 < data_row[1].length ? index + 1 : 0
48
+
49
+ next_rad_pos = next_index * Math::PI * 2 / @column_count
50
+ next_point_distance = data_row[1][next_index] * @radius
51
+ end_x = @center_x + Math::sin(next_rad_pos) * next_point_distance
52
+ end_y = @center_y - Math::cos(next_rad_pos) * next_point_distance
53
+
54
+ @d = @d.line(start_x, start_y, end_x, end_y)
55
+
56
+ @d = @d.circle(start_x, start_y, start_x - circle_radius, start_y) unless @hide_dots
57
+ end
58
+
59
+ end
60
+
61
+ @d.draw(@base_image)
62
+ end
63
+
64
+
65
+ # the lines connecting in the center, with the first line vertical
66
+ def draw_line_markers
67
+ return if @hide_line_markers
68
+
69
+
70
+ # have to do this here (AGAIN)... see draw() in this class
71
+ # because this funtion is called before the @radius, @center_x and @center_y are set
72
+ @radius = @graph_height / 2.0
73
+ @center_x = @graph_left + (@graph_width / 2.0)
74
+ @center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
75
+
76
+
77
+ # Draw horizontal line markers and annotate with numbers
78
+ @d = @d.stroke(@marker_color)
79
+ @d = @d.stroke_width 1
80
+
81
+
82
+ (0..@column_count-1).each do |index|
83
+ rad_pos = index * Math::PI * 2 / @column_count
84
+
85
+ @d = @d.line(@center_x, @center_y, @center_x + Math::sin(rad_pos) * @radius, @center_y - Math::cos(rad_pos) * @radius)
86
+
87
+
88
+ marker_label = labels[index] ? labels[index].to_s : '000'
89
+
90
+ draw_label(@center_x, @center_y, rad_pos * 360 / (2 * Math::PI), @radius, marker_label)
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def draw_label(center_x, center_y, angle, radius, amount)
97
+ r_offset = 1.1
98
+ x_offset = center_x # + 15 # The label points need to be tweaked slightly
99
+ y_offset = center_y # + 0 # This one doesn't though
100
+ x = x_offset + (radius * r_offset * Math.sin(angle.deg2rad))
101
+ y = y_offset - (radius * r_offset * Math.cos(angle.deg2rad))
102
+
103
+ # Draw label
104
+ @d.fill = @marker_color
105
+ @d.font = @font if @font
106
+ @d.pointsize = scale_fontsize(20)
107
+ @d.stroke = 'transparent'
108
+ @d.font_weight = BoldWeight
109
+ s = angle.deg2rad / (2*Math::PI)
110
+ @d.gravity = SouthGravity if s >= 0.96 or s < 0.04
111
+ @d.gravity = SouthWestGravity if s >= 0.04 or s < 0.21
112
+ @d.gravity = WestGravity if s >= 0.21 or s < 0.29
113
+ @d.gravity = NorthWestGravity if s >= 0.29 or s < 0.46
114
+ @d.gravity = NorthGravity if s >= 0.46 or s < 0.54
115
+ @d.gravity = NorthEastGravity if s >= 0.54 or s < 0.71
116
+ @d.gravity = EastGravity if s >= 0.71 or s < 0.79
117
+ @d.gravity = SouthEastGravity if s >= 0.79 or s < 0.96
118
+ # @d.gravity = NorthGravity
119
+ @d.annotate_scaled(@base_image, 0, 0, x, y, amount, @scale)
120
+ end
121
+
122
+ end
123
+
124
+
125
+ class Float
126
+ # Used for degree => radian conversions
127
+ def deg2rad
128
+ self * (Math::PI/180.0)
129
+ end
130
+ end
131
+
132
+
133
+