gruff 0.1.1 → 0.1.2

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/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
+