technical_graph 0.3.2 → 0.4.0

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.
@@ -1,8 +1,7 @@
1
1
  #encoding: utf-8
2
2
 
3
- require 'rubygems'
4
- require 'RMagick'
5
3
  require 'date'
4
+ require 'zlib'
6
5
 
7
6
  # Universal class for creating graphs/charts.
8
7
 
@@ -16,6 +15,24 @@ require 'date'
16
15
 
17
16
  class GraphImageDrawer
18
17
 
18
+ # Which type of drawing class use?
19
+ def drawing_class
20
+ if options[:drawer_class] == :rasem
21
+ require 'technical_graph/graph_image_drawer_rasem'
22
+ return GraphImageDrawerRasem
23
+ end
24
+
25
+ if options[:drawer_class] == :rmagick
26
+ require 'technical_graph/graph_image_drawer_rmagick'
27
+ return GraphImageDrawerRmagick
28
+ end
29
+ end
30
+
31
+ # Best output image format, used for testing
32
+ def best_output_format
33
+ @technical_graph.best_output_format
34
+ end
35
+
19
36
  attr_reader :technical_graph
20
37
 
21
38
  # Accessor for options Hash
@@ -38,6 +55,10 @@ class GraphImageDrawer
38
55
  @technical_graph.axis
39
56
  end
40
57
 
58
+ def logger
59
+ @technical_graph.logger
60
+ end
61
+
41
62
  def truncate_string
42
63
  options[:truncate_string]
43
64
  end
@@ -54,18 +75,25 @@ class GraphImageDrawer
54
75
  def initialize(technical_graph)
55
76
  @technical_graph = technical_graph
56
77
 
78
+ t = Time.now
79
+
80
+ # drawer type
81
+ #options[:drawer_class] ||= :rmagick
82
+ options[:drawer_class] ||= :rasem
83
+
57
84
  options[:width] ||= DEFAULT_WIDTH
58
85
  options[:height] ||= DEFAULT_HEIGHT
59
86
 
87
+ options[:axis_value_and_param_labels] = true if options[:axis_value_and_param_labels].nil?
88
+ options[:axis_zero_labels] = true if options[:axis_zero_labels].nil?
89
+
60
90
  # colors
61
91
  options[:background_color] ||= 'white'
62
92
  options[:background_hatch_color] ||= 'lightcyan2'
63
- options[:axis_color] ||= '#aaaaaa'
93
+ options[:axis_color] ||= '#000000'
64
94
 
65
95
  # antialias
66
- options[:layers_antialias] = false if options[:layers_antialias].nil?
67
- options[:axis_antialias] = false if options[:axis_antialias].nil?
68
- options[:font_antialias] = false if options[:font_antialias].nil?
96
+ options[:antialias] = false if options[:antialias].nil?
69
97
 
70
98
  # font sizes
71
99
  options[:axis_font_size] ||= 10
@@ -82,6 +110,9 @@ class GraphImageDrawer
82
110
 
83
111
  # array of all points drawn on graph, used for auto positioning of legend
84
112
  @drawn_points = Array.new
113
+
114
+ logger.debug "initializing #{self.class}"
115
+ logger.debug " TIME COST #{Time.now - t}"
85
116
  end
86
117
 
87
118
  def width
@@ -100,8 +131,8 @@ class GraphImageDrawer
100
131
  options[:height] = h.to_i if h.to_i > 0
101
132
  end
102
133
 
103
- def font_antialias
104
- options[:font_antialias] == true
134
+ def antialias
135
+ options[:antialias] == true
105
136
  end
106
137
 
107
138
  def draw_legend?
@@ -152,48 +183,17 @@ class GraphImageDrawer
152
183
  # Create background image
153
184
  def crate_blank_graph_image
154
185
  pre_image_create_calculations
155
-
156
- @image = Magick::ImageList.new
157
- @image.new_image(
158
- width,
159
- height,
160
- Magick::HatchFill.new(
161
- options[:background_color],
162
- options[:background_hatch_color]
163
- )
164
- )
165
-
166
- return @image
186
+ # create drawing proxy
187
+ @drawer = drawing_class.new(self)
167
188
  end
168
189
 
169
- attr_reader :image
190
+ attr_reader :drawer
170
191
 
171
- # Render data layer
192
+ # Render data layer, calculate coords and execute proxy
172
193
  def render_data_layer(l)
173
194
  layer_data = l.processed_data
174
195
 
175
- layer_line = Magick::Draw.new
176
- layer_text = Magick::Draw.new
177
-
178
- # global layer antialias can be override using layer option
179
- layer_antialias = l.antialias
180
- layer_antialias = options[:layers_antialias] if layer_antialias.nil?
181
-
182
- layer_line.stroke_antialias(layer_antialias)
183
- layer_line.fill(l.color)
184
- layer_line.fill_opacity(1)
185
- layer_line.stroke(l.color)
186
- layer_line.stroke_opacity(1.0)
187
- layer_line.stroke_width(1.0)
188
- layer_line.stroke_linecap('square')
189
- layer_line.stroke_linejoin('miter')
190
-
191
- layer_text.text_antialias(font_antialias)
192
- layer_text.pointsize(options[:layers_font_size])
193
- layer_text.font_family('helvetica')
194
- layer_text.font_style(Magick::NormalStyle)
195
- layer_text.text_align(Magick::LeftAlign)
196
- layer_text.text_undercolor(options[:background_color])
196
+ t = Time.now
197
197
 
198
198
  # calculate coords, draw text, and then lines and circles
199
199
  coords = Array.new
@@ -216,44 +216,19 @@ class GraphImageDrawer
216
216
  }
217
217
  end
218
218
 
219
- # labels
220
- if l.value_labels
221
- coords.each do |c|
222
- string_label = "#{truncate_string % c[:dy]}"
223
- layer_text.text(
224
- c[:ax] + 5, c[:ay],
225
- string_label
226
- )
227
- end
228
- layer_text.draw(@image)
229
- end
219
+ logger.debug "rendering layer of size #{layer_data.size}, bitmap position calculation"
220
+ logger.debug " TIME COST #{Time.now - t}"
221
+ t = Time.now
230
222
 
231
- # lines and circles
232
- coords.each do |c|
233
- # additional circle
234
- layer_line.circle(
235
- c[:ax], c[:ay],
236
- c[:ax] + 3, c[:ay]
237
- )
238
- layer_line.circle(
239
- c[:bx], c[:by],
240
- c[:bx] + 3, c[:by]
241
- )
242
-
243
- # line
244
- layer_line.line(
245
- c[:ax], c[:ay],
246
- c[:bx], c[:by]
247
- )
248
-
249
- # used for auto positioning of legend
250
- if legend_auto_position
251
- @drawn_points << { :x => c[:ax], :y => c[:ay] }
252
- @drawn_points << { :x => c[:bx], :y => c[:by] }
253
- end
254
- end
255
- layer_line.draw(@image)
223
+ # draw using proxy
224
+ drawer.render_data_layer(l, coords)
225
+ end
256
226
 
227
+ # Used for auto position for legend
228
+ def post_dot_drawn(bx, by)
229
+ if legend_auto_position
230
+ @drawn_points << { :x => bx, :y => by }
231
+ end
257
232
  end
258
233
 
259
234
  # height of 1 layer
@@ -262,7 +237,7 @@ class GraphImageDrawer
262
237
  # Choose best location
263
238
  def recalculate_legend_position
264
239
  return unless legend_auto_position
265
- puts "Auto position calculation, drawn points #{@drawn_points.size}"
240
+ logger.debug "Auto position calculation, drawn points #{@drawn_points.size}"
266
241
 
267
242
  legend_height = layers.size * ONE_LAYER_LEGEND_HEIGHT
268
243
 
@@ -278,6 +253,8 @@ class GraphImageDrawer
278
253
  { :x => width - legend_margin - legend_width, :y => height - legend_margin - legend_height }, # bottom-right
279
254
  ]
280
255
 
256
+ t = Time.now
257
+
281
258
  # calculate nearest distance of all drawn points
282
259
  positions.each do |p|
283
260
  p[:distance] = (width ** 2 + height ** 2) ** 0.5 # max distance, diagonal of graph
@@ -291,71 +268,77 @@ class GraphImageDrawer
291
268
  end
292
269
  end
293
270
 
294
- # chose position with hihest distance
271
+ logger.debug "auto legend best position distance calc."
272
+ logger.debug " TIME COST #{Time.now - t}"
273
+ t = Time.now
274
+
275
+ # chose position with highest distance
295
276
  positions.sort! { |a, b| a[:distance] <=> b[:distance] }
296
277
  best_position = positions.last
297
278
  options[:legend_x] = best_position[:x]
298
279
  options[:legend_y] = best_position[:y]
299
280
 
300
- puts "Best position x #{options[:legend_x]}, y #{options[:legend_y]}, distance #{best_position[:distance]}"
301
- # puts positions.to_yaml
281
+ logger.debug "Best position x #{options[:legend_x]}, y #{options[:legend_y]}, distance #{best_position[:distance]}"
282
+ logger.debug " TIME COST #{Time.now - t}"
302
283
  end
303
284
 
304
285
  # Render legend on graph
305
286
  def render_data_legend
306
287
  return unless draw_legend?
307
-
308
288
  recalculate_legend_position
309
289
 
310
- legend_text = Magick::Draw.new
311
- legend_text_antialias = options[:layers_font_size]
312
- legend_text.stroke_antialias(legend_text_antialias)
313
- legend_text.text_antialias(legend_text_antialias)
314
- legend_text.pointsize(options[:axis_font_size])
315
- legend_text.font_family('helvetica')
316
- legend_text.font_style(Magick::NormalStyle)
317
- legend_text.text_align(Magick::LeftAlign)
318
- legend_text.text_undercolor(options[:background_color])
319
-
320
290
  x = legend_x
321
291
  y = legend_y
322
292
 
323
- layers.each do |l|
324
- legend_text.fill(l.color)
325
-
326
- string_label = l.label
327
- legend_text.text(
328
- x, y,
329
- string_label
330
- )
293
+ legend_data = Array.new
331
294
 
332
- # little dot
333
- legend_text.circle(
334
- x - 10, y,
335
- x - 10 + 3, y
336
- )
295
+ layers.each do |l|
296
+ h = Hash.new
297
+ h[:color] = l.color
298
+ h[:label] = l.label
299
+ h[:x] = x
300
+ h[:y] = y
337
301
 
302
+ legend_data << h
338
303
  y += ONE_LAYER_LEGEND_HEIGHT
339
304
  end
340
- legend_text.draw(@image)
305
+
306
+ drawer.legend(legend_data)
341
307
  end
342
308
 
343
309
  # Save output to file
344
310
  def save_to_file(file)
345
- @image.write(file)
311
+ t = Time.now
312
+
313
+ drawer.save(file)
314
+
315
+ logger.debug "saving image"
316
+ logger.debug " TIME COST #{Time.now - t}"
346
317
  end
347
318
 
348
319
  # Export image
349
320
  def to_format(format)
350
- i = @image.flatten_images
351
- i.format = format
352
- return i.to_blob
321
+ t = Time.now
322
+
323
+ blob = drawer.to_format(format)
324
+
325
+ logger.debug "exporting image as string"
326
+ logger.debug " TIME COST #{Time.now - t}"
327
+
328
+ return blob
353
329
  end
354
330
 
355
331
  # Return binary PNG
356
332
  def to_png
357
- to_format('png')
333
+ drawer.to_png
334
+ end
335
+
336
+ def to_svg
337
+ drawer.to_svg
358
338
  end
359
339
 
340
+ def to_svgz
341
+ drawer.to_svgz
342
+ end
360
343
 
361
344
  end
@@ -0,0 +1,72 @@
1
+ #encoding: utf-8
2
+
3
+ module GraphImageDrawerModule
4
+
5
+ def initialize(drawer)
6
+ @drawer = drawer
7
+ create_blank_image
8
+ end
9
+
10
+ attr_reader :drawer
11
+
12
+ def width
13
+ drawer.width
14
+ end
15
+
16
+ def height
17
+ drawer.height
18
+ end
19
+
20
+ def truncate_string
21
+ drawer.truncate_string
22
+ end
23
+
24
+ def options
25
+ drawer.options
26
+ end
27
+
28
+ def logger
29
+ drawer.logger
30
+ end
31
+
32
+
33
+ # Draw one or many axis
34
+ def x_axis(x_array, options = { :color => 'black', :width => 1 })
35
+ axis(x_array, [], options)
36
+ end
37
+
38
+ # Draw one or many axis
39
+ def y_axis(y_array, options = { :color => 'black', :width => 1 })
40
+ axis([], y_array, options)
41
+ end
42
+
43
+ # Return binary PNG
44
+ def to_png
45
+ to_format('png')
46
+ end
47
+
48
+ def to_svg
49
+ to_format('svg')
50
+ end
51
+
52
+ def to_svgz
53
+ drawer.deflate_string( to_format('svg') )
54
+ end
55
+
56
+ def deflate_string(str, level = 9)
57
+ z = Zlib::Deflate.new(level)
58
+ dst = z.deflate(str, Zlib::FINISH)
59
+ z.close
60
+ dst
61
+ end
62
+
63
+ def format_from_filename(file)
64
+ file.gsub(/^.*\./, '')
65
+ end
66
+
67
+ # Used for creating temp files
68
+ def random_filename
69
+ (0...16).map{65.+(rand(25)).chr}.join
70
+ end
71
+
72
+ end
@@ -0,0 +1,185 @@
1
+ #encoding: utf-8
2
+
3
+ require 'technical_graph/graph_image_drawer_module'
4
+ require 'rubygems'
5
+ require 'rasem'
6
+ require 'tmpdir'
7
+
8
+ class GraphImageDrawerRasem
9
+ include GraphImageDrawerModule
10
+
11
+ # Initialize blank image
12
+ def create_blank_image
13
+ @image = Rasem::SVGImage.new(drawer.width, drawer.height)
14
+ end
15
+
16
+ # Draw both array axis
17
+ def axis(x_array, y_array, _options = { :color => 'black', :width => 1 }, render_labels = false, x_labels = [], y_labels = [])
18
+ # for single axis
19
+ x_array = [x_array] if not x_array.kind_of? Array
20
+ y_array = [y_array] if not y_array.kind_of? Array
21
+
22
+ _s = self
23
+
24
+ @image.group :stroke => _options[:color], :stroke_width => _options[:width] do
25
+ x_array.each_with_index do |x, i|
26
+ line(x, 0, x, _s.height, { })
27
+
28
+ # labels
29
+ label = x_labels[i]
30
+ if render_labels and not label.nil?
31
+ label = "#{_s.truncate_string % label}"
32
+ text(x + 15, _s.height - 15, label, { })
33
+ end
34
+ end
35
+
36
+ y_array.each_with_index do |y, i|
37
+ line(0, y, _s.width, y, { })
38
+
39
+ # labels
40
+ label = y_labels[i]
41
+ if render_labels and not label.nil?
42
+ label = "#{_s.truncate_string % label}"
43
+ text(15, y + 15, label, { })
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # Label for parameters and values
50
+ def axis_labels(parameter_label, value_label, _options = { :color => 'black', :width => 1, :size => 20 })
51
+ _s = self
52
+ @image.group :stroke => _options[:color], :stroke_width => _options[:width] do
53
+ text(
54
+ (_s.width / 2).to_i,
55
+ _s.height - 40,
56
+ parameter_label, { 'font-size' => "#{_options[:size]}px" }
57
+ )
58
+
59
+ text(
60
+ (_s.height / 2).to_i,
61
+ -40,
62
+ value_label, { :transform => 'rotate(90 0,0)', 'font-size' => "#{_options[:size]}px" }
63
+ )
64
+ end
65
+ end
66
+
67
+ def render_data_layer(l, coords)
68
+ _s = self
69
+ _l = l
70
+ _coords = coords
71
+
72
+ # value labels
73
+ if l.value_labels
74
+ t = Time.now
75
+
76
+ @image.group :stroke => _s.options[:axis_color], :stroke_width => 1 do
77
+ _coords.each do |c|
78
+ string_label = "#{_s.truncate_string % c[:dy]}"
79
+ text(
80
+ c[:ax] + 5, c[:ay],
81
+ string_label
82
+ )
83
+ end
84
+ end
85
+
86
+ logger.debug "labels"
87
+ logger.debug " TIME COST #{Time.now - t}"
88
+ end
89
+
90
+ t = Time.now
91
+
92
+ # lines and dots
93
+ @image.group :stroke => l.color, :stroke_width => 1 do
94
+ _coords.each do |c|
95
+ # additional circle
96
+ circle(c[:ax], c[:ay], 2, { :fill => l.color })
97
+ circle(c[:bx], c[:by], 2, { :fill => l.color })
98
+ # line
99
+ line(
100
+ c[:ax], c[:ay],
101
+ c[:bx], c[:by],
102
+ { }
103
+ )
104
+
105
+ _s.drawer.post_dot_drawn(c[:ax], c[:ay])
106
+ _s.drawer.post_dot_drawn(c[:bx], c[:by])
107
+ end
108
+ end
109
+
110
+ logger.debug "dots and lines"
111
+ logger.debug " TIME COST #{Time.now - t}"
112
+ end
113
+
114
+ def legend(legend_data)
115
+ _s = self
116
+
117
+ @image.group :stroke_width => 1, :stroke => '' do
118
+ legend_data.each do |l|
119
+ circle(l[:x], l[:y], 2, { :stroke => l[:color], :fill => l[:color] })
120
+ text(l[:x] + 5, l[:y], l[:label], { :stroke => l[:color], :fill => l[:color] })
121
+ end
122
+ end
123
+ end
124
+
125
+ # Needed before saving?
126
+ def close
127
+ @image.close if not closed?
128
+ @closed = true
129
+ end
130
+
131
+ def closed?
132
+ @closed
133
+ end
134
+
135
+ # Save to file, convert when needed
136
+ def save(file)
137
+ close
138
+
139
+ format = format_from_filename(file)
140
+ case format
141
+ when 'svg' then
142
+ string = to_svg
143
+ when 'svgz' then
144
+ string = to_svgz
145
+ else
146
+ # ugly hack, save to svg and then convert using image magick
147
+ tmp_file = file.gsub(/#{format}/, 'svg')
148
+ # save to svg
149
+ save(tmp_file)
150
+ # convert
151
+ `convert "#{tmp_file}" "#{file}"`
152
+ return
153
+ end
154
+
155
+ File.open(file, 'w') do |f|
156
+ f << string
157
+ end
158
+ end
159
+
160
+
161
+ def to_format(format)
162
+ close
163
+
164
+ return @image.output if format == 'svg'
165
+ return to_svgz if format == 'svgz'
166
+
167
+ #raise 'Not implemented' if not format == 'svg'
168
+ return ugly_convert(format)
169
+ end
170
+
171
+ # Ugly, save temporary file, convert, read, delete temp file
172
+ def ugly_convert(format)
173
+ # create temp file
174
+ tmp_file = File.join(Dir.tmpdir, "#{random_filename}.#{format}")
175
+ save(tmp_file)
176
+ # read content
177
+ contents = open(tmp_file, "rb") { |io| io.read }
178
+ # remove temp file
179
+ File.delete(tmp_file)
180
+
181
+ # return content
182
+ contents
183
+ end
184
+
185
+ end