svg-graph 1.0.1 → 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data.tar.gz.sig +0 -0
  2. data/History.txt +7 -5
  3. data/Manifest.txt +53 -17
  4. data/README.markdown +14 -0
  5. data/README.txt +10 -3
  6. data/Rakefile +6 -0
  7. data/build.xml +143 -0
  8. data/dist.xml.in +46 -0
  9. data/images/bar.png +0 -0
  10. data/images/bar.svg +76 -0
  11. data/images/bar.svgz +0 -0
  12. data/images/barhorizontal.png +0 -0
  13. data/images/barhorizontal.svg +76 -0
  14. data/images/barhorizontal.svgz +0 -0
  15. data/images/line.png +0 -0
  16. data/images/line.svg +80 -0
  17. data/images/line.svgz +0 -0
  18. data/images/pie.png +0 -0
  19. data/images/pie.svg +70 -0
  20. data/images/pie.svgz +0 -0
  21. data/images/plot.png +0 -0
  22. data/images/plot.svg +131 -0
  23. data/images/plot.svgz +0 -0
  24. data/images/schedule.png +0 -0
  25. data/images/schedule.svg +67 -0
  26. data/images/timeseries.png +0 -0
  27. data/images/timeseries.svg +179 -0
  28. data/images/timeseries.svgz +0 -0
  29. data/index.xml +281 -0
  30. data/install.rb +161 -0
  31. data/lib/SVG/Graph/Bar.rb +1 -1
  32. data/lib/SVG/Graph/Graph.rb +11 -9
  33. data/lib/SVG/Graph/Line.rb +1 -0
  34. data/lib/SVG/Graph/Pie.rb +3 -3
  35. data/lib/SVG/Graph/Plot.rb +69 -20
  36. data/lib/SVG/Graph/Schedule.rb +19 -5
  37. data/lib/SVG/Graph/TimeSeries.rb +62 -45
  38. data/lib/svggraph.rb +1 -2
  39. data/screenshots.xml +148 -0
  40. data/style/common.xsl +37 -0
  41. data/style/release_html.xsl +169 -0
  42. data/style/release_txt.xsl +25 -0
  43. data/svg-graph.gemspec +59 -0
  44. data/test/data.txt +4 -0
  45. data/test/plot.rb +51 -0
  46. data/test/schedule.rb +43 -0
  47. data/test/single.rb +33 -0
  48. data/test/test.rb +54 -0
  49. data/test/test_plot.rb +170 -0
  50. data/test/test_svg_graph.rb +24 -55
  51. data/test/timeseries.rb +58 -0
  52. metadata +79 -14
  53. metadata.gz.sig +2 -1
@@ -140,6 +140,7 @@ module SVG
140
140
  :title_font_size =>16,
141
141
  :subtitle_font_size =>14,
142
142
  :x_label_font_size =>12,
143
+ :y_label_font_size =>12,
143
144
  :x_title_font_size =>14,
144
145
  :y_label_font_size =>12,
145
146
  :y_title_font_size =>14,
@@ -365,14 +366,14 @@ module SVG
365
366
  def calculate_left_margin
366
367
  @border_left = 7
367
368
  # Check for Y labels
368
- max_y_label_height_px = rotate_y_labels ?
369
- y_label_font_size :
369
+ max_y_label_height_px = @rotate_y_labels ?
370
+ @y_label_font_size :
370
371
  get_y_labels.max{|a,b|
371
372
  a.to_s.length<=>b.to_s.length
372
- }.to_s.length * y_label_font_size * 0.6
373
- @border_left += max_y_label_height_px if show_y_labels
374
- @border_left += max_y_label_height_px + 10 if stagger_y_labels
375
- @border_left += y_title_font_size + 5 if show_y_title
373
+ }.to_s.length * @y_label_font_size * 0.6
374
+ @border_left += max_y_label_height_px if @show_y_labels
375
+ @border_left += max_y_label_height_px + 10 if @stagger_y_labels
376
+ @border_left += y_title_font_size + 5 if @show_y_title
376
377
  end
377
378
 
378
379
 
@@ -533,7 +534,7 @@ module SVG
533
534
 
534
535
  x = count * label_width + x_label_offset( label_width )
535
536
  y = @graph_height + x_label_font_size + 3
536
- t = 0 - (font_size / 2)
537
+ #t = 0 - (font_size / 2)
537
538
 
538
539
  if stagger_x_labels and count % 2 == 1
539
540
  y += stagger
@@ -700,7 +701,8 @@ module SVG
700
701
  end
701
702
 
702
703
  def keys
703
- return @data.collect{ |d| d[:title] }
704
+ i = 0
705
+ return @data.collect{ |d| i+=1; d[:title] || "Serie #{i}" }
704
706
  end
705
707
 
706
708
  # Draws the legend on the graph
@@ -792,7 +794,7 @@ module SVG
792
794
  css = get_style
793
795
  rv = {}
794
796
  while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
795
- names_orig = names = $1
797
+ names = $1
796
798
  css = $'
797
799
  css =~ /([^}]+)\}/m
798
800
  content = $1
@@ -102,6 +102,7 @@ module SVG
102
102
  :area_fill => false
103
103
  )
104
104
 
105
+
105
106
  self.top_align = self.top_font = self.right_align = self.right_font = 1
106
107
  end
107
108
 
data/lib/SVG/Graph/Pie.rb CHANGED
@@ -170,11 +170,11 @@ module SVG
170
170
 
171
171
  def keys
172
172
  total = 0
173
- max_value = 0
173
+ #max_value = 0
174
174
  @data.each {|x| total += x }
175
175
  percent_scale = 100.0 / total
176
176
  count = -1
177
- a = @config[:fields].collect{ |x|
177
+ @config[:fields].collect{ |x|
178
178
  count += 1
179
179
  v = @data[count]
180
180
  perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""
@@ -275,7 +275,7 @@ module SVG
275
275
  msr = Math.sin(radians)
276
276
  mcr = Math.cos(radians)
277
277
  tx = radius + (msr * radius)
278
- ty = radius -(mcr * radius)
278
+ ty = radius - (mcr * radius)
279
279
 
280
280
  if expanded or (expand_greatest && value == max_value)
281
281
  tx += (msr * expand_gap)
@@ -97,7 +97,9 @@ module SVG
97
97
  :show_data_values => true,
98
98
  :show_data_points => true,
99
99
  :area_fill => false,
100
- :stacked => false
100
+ :stacked => false,
101
+ :show_lines => true,
102
+ :round_popups => true
101
103
  )
102
104
  self.top_align = self.right_align = self.top_font = self.right_font = 1
103
105
  end
@@ -127,14 +129,22 @@ module SVG
127
129
  attr_accessor :show_data_points
128
130
  # Set the minimum value of the X axis
129
131
  attr_accessor :min_x_value
132
+ # Set the maximum value of the X axis
133
+ attr_accessor :max_x_value
130
134
  # Set the minimum value of the Y axis
131
135
  attr_accessor :min_y_value
136
+ # Set the maximum value of the Y axis
137
+ attr_accessor :max_y_value
138
+ # Show lines connecting data points
139
+ attr_accessor :show_lines
140
+ # Round value of data points in popups to integer
141
+ attr_accessor :round_popups
132
142
 
133
143
 
134
144
  # Adds data to the plot. The data must be in X,Y pairs; EG
135
145
  # [ 1, 2 ] # A data set with 1 point: (1,2)
136
146
  # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
137
- def add_data data
147
+ def add_data(data)
138
148
 
139
149
  @data = [] unless @data
140
150
 
@@ -145,17 +155,21 @@ module SVG
145
155
  "data points" unless data[:data].length % 2 == 0
146
156
  return if data[:data].length == 0
147
157
 
158
+ data[:description] ||= Array.new(data[:data].size/2)
159
+ if data[:description].size != data[:data].size/2
160
+ raise "Description for popups does not have same size as provided data: #{data[:description].size} vs #{data[:data].size/2}"
161
+ end
162
+
148
163
  x = []
149
164
  y = []
150
165
  data[:data].each_index {|i|
151
166
  (i%2 == 0 ? x : y) << data[:data][i]
152
167
  }
153
- sort( x, y )
168
+ sort( x, y, data[:description] )
154
169
  data[:data] = [x,y]
155
170
  @data << data
156
171
  end
157
172
 
158
-
159
173
  protected
160
174
 
161
175
  def keys
@@ -177,10 +191,22 @@ module SVG
177
191
 
178
192
  X = 0
179
193
  Y = 1
180
- def x_range
194
+
195
+ def max_x_range
181
196
  max_value = @data.collect{|x| x[:data][X][-1] }.max
197
+ max_value = max_value > max_x_value ? max_value : max_x_value if max_x_value
198
+ max_value
199
+ end
200
+
201
+ def min_x_range
182
202
  min_value = @data.collect{|x| x[:data][X][0] }.min
183
- min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
203
+ min_value = min_value < min_x_value ? min_value : min_x_value if min_x_value
204
+ min_value
205
+ end
206
+
207
+ def x_range
208
+ max_value = max_x_range
209
+ min_value = min_x_range
184
210
 
185
211
  range = max_value - min_value
186
212
  right_pad = range == 0 ? 10 : range / 20.0
@@ -205,17 +231,28 @@ module SVG
205
231
 
206
232
  def field_width
207
233
  values = get_x_values
208
- max = @data.collect{|x| x[:data][X][-1]}.max
234
+ max = max_x_range
209
235
  dx = (max - values[-1]).to_f / (values[-1] - values[-2])
210
236
  (@graph_width.to_f - font_size*2*right_font) /
211
237
  (values.length + dx - right_align)
212
238
  end
213
239
 
214
240
 
215
- def y_range
241
+ def max_y_range
216
242
  max_value = @data.collect{|x| x[:data][Y].max }.max
243
+ max_value = max_value > max_y_value ? max_value : max_y_value if max_y_value
244
+ max_value
245
+ end
246
+
247
+ def min_y_range
217
248
  min_value = @data.collect{|x| x[:data][Y].min }.min
218
- min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
249
+ min_value = min_value < min_y_value ? min_value : min_y_value if min_y_value
250
+ min_value
251
+ end
252
+
253
+ def y_range
254
+ max_value = max_y_range
255
+ min_value = min_y_range
219
256
 
220
257
  range = max_value - min_value
221
258
  top_pad = range == 0 ? 10 : range / 20.0
@@ -232,15 +269,21 @@ module SVG
232
269
 
233
270
  def get_y_values
234
271
  min_value, max_value, scale_division = y_range
272
+ if max_value != min_value
273
+ while (max_value - min_value) < scale_division
274
+ scale_division /= 10.0
275
+ end
276
+ end
235
277
  rv = []
236
278
  min_value.step( max_value, scale_division ) {|v| rv << v}
279
+ rv << rv[0] + 1 if rv.length == 1
237
280
  return rv
238
281
  end
239
282
  alias :get_y_labels :get_y_values
240
283
 
241
284
  def field_height
242
285
  values = get_y_values
243
- max = @data.collect{|x| x[:data][Y].max }.max
286
+ max = max_y_range
244
287
  if values.length == 1
245
288
  dx = values[-1]
246
289
  else
@@ -252,9 +295,9 @@ module SVG
252
295
 
253
296
  def draw_data
254
297
  line = 1
255
-
256
- x_min, x_max, x_div = x_range
257
- y_min, y_max, y_div = y_range
298
+
299
+ x_min, x_max = x_range
300
+ y_min, y_max = y_range
258
301
  x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
259
302
  y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
260
303
 
@@ -279,10 +322,12 @@ module SVG
279
322
  })
280
323
  end
281
324
 
282
- @graph.add_element( "path", {
283
- "d" => "M#{x_start} #{y_start} #{lpath}",
284
- "class" => "line#{line}"
285
- })
325
+ if show_lines
326
+ @graph.add_element( "path", {
327
+ "d" => "M#{x_start} #{y_start} #{lpath}",
328
+ "class" => "line#{line}"
329
+ })
330
+ end
286
331
 
287
332
  if show_data_points || show_data_values
288
333
  x_points.each_index { |idx|
@@ -295,7 +340,7 @@ module SVG
295
340
  "r" => "2.5",
296
341
  "class" => "dataPoint#{line}"
297
342
  })
298
- add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
343
+ add_popup(x, y, format( x_points[idx], y_points[idx], data[:description][idx])) if add_popups
299
344
  end
300
345
  make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
301
346
  }
@@ -304,8 +349,12 @@ module SVG
304
349
  end
305
350
  end
306
351
 
307
- def format x, y
308
- "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
352
+ def format x, y, desc
353
+ info = []
354
+ info << (round_popups ? (x * 100).to_i / 100 : x)
355
+ info << (round_popups ? (y * 100).to_i / 100 : y)
356
+ info << desc
357
+ "(#{info.compact.join(', ')})"
309
358
  end
310
359
 
311
360
  def get_css
@@ -1,5 +1,8 @@
1
1
  require 'SVG/Graph/Plot'
2
- require 'parsedate'
2
+ TIME_PARSE_AVAIL = (RUBY_VERSION =~ /1\.9\./) ? true : false
3
+ if not TIME_PARSE_AVAIL then
4
+ require 'parsedate'
5
+ end
3
6
 
4
7
  module SVG
5
8
  module Graph
@@ -159,8 +162,13 @@ module SVG
159
162
  if im3 == 0
160
163
  y << data[:data][i]
161
164
  else
162
- arr = ParseDate.parsedate( data[:data][i] )
163
- t = Time.local( *arr[0,6].compact )
165
+ if TIME_PARSE_AVAIL then
166
+ arr = DateTime.parse(data[:data][i])
167
+ t = arr.to_time
168
+ else
169
+ arr = ParseDate.parsedate( data[:data][i] )
170
+ t = Time.local( *arr[0,6].compact )
171
+ end
164
172
  (im3 == 1 ? x_start : x_end) << t.to_i
165
173
  end
166
174
  }
@@ -172,8 +180,14 @@ module SVG
172
180
  protected
173
181
 
174
182
  def min_x_value=(value)
175
- arr = ParseDate.parsedate( value )
176
- @min_x_value = Time.local( *arr[0,6].compact ).to_i
183
+ if TIME_PARSE_AVAIL then
184
+ arr = Time.parse(value)
185
+ t = arr.to_time
186
+ else
187
+ arr = ParseDate.parsedate( value )
188
+ t = Time.local( *arr[0,6].compact )
189
+ end
190
+ @min_x_value = t.to_i
177
191
  end
178
192
 
179
193
 
@@ -1,5 +1,8 @@
1
1
  require 'SVG/Graph/Plot'
2
- require 'parsedate'
2
+ TIME_PARSE_AVAIL = (RUBY_VERSION =~ /1\.9\./) ? true : false
3
+ if not TIME_PARSE_AVAIL then
4
+ require 'parsedate'
5
+ end
3
6
 
4
7
  module SVG
5
8
  module Graph
@@ -7,15 +10,17 @@ module SVG
7
10
  #
8
11
  # = Synopsis
9
12
  #
10
- # require 'SVG/Graph/TimeSeriess'
11
- #
13
+ # require 'SVG/Graph/TimeSeries'
14
+ #
12
15
  # # Data sets are x,y pairs
13
- # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11,
14
- # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]
15
- # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4,
16
- # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6,
17
- # "5/1/84", 17, "10/1/80", 12]
18
- #
16
+ # projection = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11,
17
+ # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]
18
+ # actual = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4,
19
+ # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6,
20
+ # "5/1/84", 17, "10/1/80", 12]
21
+ #
22
+ # title = "Ice Cream Cone Consumption"
23
+ #
19
24
  # graph = SVG::Graph::TimeSeries.new( {
20
25
  # :width => 640,
21
26
  # :height => 480,
@@ -27,7 +32,7 @@ module SVG
27
32
  # :scale_y_integers => true,
28
33
  # :min_x_value => 0,
29
34
  # :min_y_value => 0,
30
- # :show_data_labels => true,
35
+ # :show_data_values => true,
31
36
  # :show_x_guidelines => true,
32
37
  # :show_x_title => true,
33
38
  # :x_title => "Time",
@@ -39,13 +44,13 @@ module SVG
39
44
  # })
40
45
  #
41
46
  # graph.add_data({
42
- # :data => projection
43
- # :title => 'Projected',
47
+ # :data => projection,
48
+ # :title => 'Projected',
44
49
  # })
45
- #
50
+ #
46
51
  # graph.add_data({
47
- # :data => actual,
48
- # :title => 'Actual',
52
+ # :data => actual,
53
+ # :title => 'Actual',
49
54
  # })
50
55
  #
51
56
  # print graph.burn()
@@ -140,46 +145,35 @@ module SVG
140
145
  # :title => 'Two'
141
146
  # )
142
147
  #
143
- # Note that the data must be in time,value pairs, and that the date format
144
- # may be any date that is parseable by ParseDate.
148
+ # Note that the data must be in time,value pairs. The time may be any date in
149
+ # a format that is parseable by ParseDate, a Time object, or a number of seconds
150
+ # after the unix epoch.
145
151
  def add_data data
146
- @data = [] unless @data
147
-
148
- raise "No data provided by #{@data.inspect}" unless data[:data] and
149
- data[:data].kind_of? Array
150
- raise "Data supplied must be x,y pairs! "+
151
- "The data provided contained an odd set of "+
152
- "data points" unless data[:data].length % 2 == 0
153
- return if data[:data].length == 0
154
-
155
-
156
- x = []
157
- y = []
158
- data[:data].each_index {|i|
159
- if i%2 == 0
160
- arr = ParseDate.parsedate( data[:data][i] )
161
- t = Time.local( *arr[0,6].compact )
162
- x << t.to_i
163
- else
164
- y << data[:data][i]
165
- end
166
- }
167
- sort( x, y )
168
- data[:data] = [x,y]
169
- @data << data
152
+ data[:data].each_index do |i|
153
+ data[:data][i] = parse_time(data[:data][i]).to_i if i % 2 == 0
154
+ end
155
+ super(data)
170
156
  end
171
157
 
172
158
 
173
159
  protected
174
160
 
175
161
  def min_x_value=(value)
176
- arr = ParseDate.parsedate( value )
177
- @min_x_value = Time.local( *arr[0,6].compact ).to_i
162
+ t = parse_time(value)
163
+ @min_x_value = t.to_i
178
164
  end
179
165
 
166
+ def max_x_value=(value)
167
+ t = parse_time(value)
168
+ @max_x_value = t.to_i
169
+ end
180
170
 
181
- def format x, y
182
- Time.at( x ).strftime( popup_format )
171
+ def format x, y, description
172
+ info = [
173
+ Time.at(x).strftime(popup_format),
174
+ round_popups ? (y * 100).to_i / 100 : y,
175
+ description
176
+ ].compact.join(', ')
183
177
  end
184
178
 
185
179
  def get_x_labels
@@ -187,6 +181,29 @@ module SVG
187
181
  end
188
182
 
189
183
  private
184
+
185
+ # Accepts date time as a string, number of seconds since the epoch, or Time
186
+ # object and returns a Time object. Raises an error if not a valid date time
187
+ # representation.
188
+ def parse_time(time)
189
+ case time
190
+ when Time
191
+ return time
192
+ when String
193
+ if TIME_PARSE_AVAIL then
194
+ arr = DateTime.parse(time)
195
+ return arr.to_time
196
+ else
197
+ arr = ParseDate.parsedate(time)
198
+ return Time.local( *arr[0,6].compact )
199
+ end
200
+ when Integer
201
+ return Time.at(time)
202
+ else
203
+ raise "Can not parse time #{time.inspect}"
204
+ end
205
+ end
206
+
190
207
  def get_x_values
191
208
  rv = []
192
209
  min, max, scale_division = x_range