technical_graph 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,11 +1,16 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem 'rmagick'
4
3
  gem 'rasem'
5
4
 
5
+ # optional gem
6
+ if Gem.source_index.find_name('rmagick').size > 0
7
+ gem 'rmagick'
8
+ end
9
+
6
10
  # Add dependencies to develop your gem here.
7
11
  # Include everything needed to run rake, tests, features, etc.
8
12
  group :development do
13
+ gem "rdoc"
9
14
  gem "shoulda"
10
15
  gem "bundler", "~> 1.0.0"
11
16
  gem "rspec"
data/Gemfile.lock CHANGED
@@ -7,18 +7,20 @@ GEM
7
7
  bundler (~> 1.0)
8
8
  git (>= 1.2.5)
9
9
  rake
10
- rake (0.9.2)
10
+ json (1.6.1)
11
+ rake (0.9.2.2)
11
12
  rasem (0.6.1)
12
- rcov (0.9.10)
13
- rmagick (2.13.1)
14
- rspec (2.6.0)
15
- rspec-core (~> 2.6.0)
16
- rspec-expectations (~> 2.6.0)
17
- rspec-mocks (~> 2.6.0)
18
- rspec-core (2.6.4)
19
- rspec-expectations (2.6.0)
13
+ rcov (0.9.11)
14
+ rdoc (3.11)
15
+ json (~> 1.4)
16
+ rspec (2.7.0)
17
+ rspec-core (~> 2.7.0)
18
+ rspec-expectations (~> 2.7.0)
19
+ rspec-mocks (~> 2.7.0)
20
+ rspec-core (2.7.1)
21
+ rspec-expectations (2.7.0)
20
22
  diff-lcs (~> 1.1.2)
21
- rspec-mocks (2.6.0)
23
+ rspec-mocks (2.7.0)
22
24
  shoulda (2.11.3)
23
25
 
24
26
  PLATFORMS
@@ -29,6 +31,6 @@ DEPENDENCIES
29
31
  jeweler
30
32
  rasem
31
33
  rcov
32
- rmagick
34
+ rdoc
33
35
  rspec
34
36
  shoulda
data/Rakefile CHANGED
@@ -58,7 +58,7 @@ end
58
58
 
59
59
  task :default => :test
60
60
 
61
- require 'rake/rdoctask'
61
+ require 'rdoc/task'
62
62
  Rake::RDocTask.new do |rdoc|
63
63
  version = File.exist?('VERSION') ? File.read('VERSION') : ""
64
64
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.5.0
@@ -30,7 +30,7 @@ class DataLayer
30
30
  @data_params = options
31
31
 
32
32
  @data_params[:color] ||= GraphColorLibrary.instance.get_color
33
- @data_params[:label] ||= ''
33
+ @data_params[:label] ||= ' '
34
34
  # default true, write values near dots
35
35
  @data_params[:value_labels] = false if options[:value_labels] == false
36
36
 
@@ -6,13 +6,18 @@ module DataLayerProcessorNoiseRemoval
6
6
  DEFAULT_NOISE_REMOVAL_LEVEL = 3
7
7
  DEFAULT_NOISE_REMOVAL_WINDOW_SIZE = 10
8
8
 
9
+ NOISE_COEFF = 1000
10
+ NOISE_POWER_COEFF = 8
11
+
9
12
  def noise_removal_initialize(options)
10
13
  @noise_removal = options[:noise_removal] == true
11
14
  @noise_removal_level = options[:noise_removal_level] || DEFAULT_NOISE_REMOVAL_LEVEL
12
15
  @noise_removal_window_size = options[:noise_removal_window_size] || DEFAULT_NOISE_REMOVAL_WINDOW_SIZE
16
+
17
+ @noise_threshold = Math.log(NOISE_COEFF / @noise_removal_level)
13
18
  end
14
19
 
15
- attr_accessor :noise_removal_level, :noise_removal_window_size, :noise_removal
20
+ attr_accessor :noise_removal_level, :noise_removal_window_size, :noise_removal, :noise_threshold
16
21
 
17
22
  # Smooth values
18
23
  def noise_removal_process
@@ -53,19 +58,50 @@ module DataLayerProcessorNoiseRemoval
53
58
  i_from = noise_removal_window_from(i)
54
59
  i_to = noise_removal_window_to(i)
55
60
 
56
- part_array = data.clone_partial_w_fill(i_from, i_to)
57
- y_mean = part_array.collect { |p| p.y }.float_mean
58
61
 
59
- # another algorithm
60
- noise_strength = (data[i].y - y_mean).abs / y_mean
61
- return noise_strength_enough?(noise_strength)
62
+ # y_mean = part_array.collect { |p| p.y }.float_mean
63
+ # # another algorithm
64
+ # noise_strength = (data[i].y - y_mean).abs / y_mean
65
+ # return noise_strength_enough?(noise_strength)
66
+
67
+ # calc. avg 'derivative'
68
+ avg_der = calc_avg_derivative(i_from, i_to)
69
+ current_der = calc_avg_derivative(i-1, i+1)
70
+
71
+ # safety
72
+ return false if avg_der == 0 or current_der == 0
73
+
74
+ begin
75
+ current_level = Math.log((current_der / avg_der) ** NOISE_POWER_COEFF).abs
76
+ rescue Errno::EDOM
77
+ # can not calculate logarithm
78
+ return false
79
+ rescue Errno::ERANGE
80
+ # can not calculate logarithm
81
+ return false
82
+ end
83
+ logger.debug "noise removal, avg der #{avg_der}, current #{current_der}, current lev #{current_level}, threshold #{noise_threshold}"
84
+ return current_level > noise_threshold
62
85
  end
63
86
 
64
- # Some magic here, beware
65
- def noise_strength_enough?(noise_strength)
66
- threshold_strength = Math.log(@noise_removal_level)
67
- return noise_strength > threshold_strength
87
+ def calc_avg_derivative(i_from, i_to)
88
+ part_array = data.clone_partial_w_fill(i_from, i_to)
89
+ derivatives = Array.new
90
+ (1...part_array.size).each do |i|
91
+ x_len = (part_array[i].x - part_array[i - 1].x).abs
92
+ y_len = (part_array[i].y - part_array[i - 1].y).abs
93
+ derivatives << (x_len / y_len).abs if x_len.abs > 0
94
+ end
95
+ avg_der = derivatives.float_mean
96
+ return avg_der
68
97
  end
69
98
 
99
+ ## Some magic here, beware
100
+ #def noise_strength_enough?(noise_strength)
101
+ # threshold_strength = Math.log(@noise_removal_level)
102
+ # logger.debug "Noise removal noise str #{noise_strength}, threshold #{threshold_strength}"
103
+ # return noise_strength > threshold_strength
104
+ #end
105
+
70
106
 
71
107
  end
@@ -58,6 +58,10 @@ class GraphAxis
58
58
  options[:x_axis_interval]
59
59
  end
60
60
 
61
+ def adjust_axis_to_zero
62
+ options[:adjust_axis_to_zero]
63
+ end
64
+
61
65
  # Where to put axis values
62
66
  def value_axis
63
67
  return calc_axis(data_processor.y_min, data_processor.y_max, options[:y_axis_interval], options[:y_axis_count], y_axis_fixed?)
@@ -81,6 +85,8 @@ class GraphAxis
81
85
  axis << current
82
86
  current += interval
83
87
  end
88
+ axis = move_axis_to_fit_zero(axis) if adjust_axis_to_zero
89
+
84
90
  logger.debug "fixed interval axis calculation from #{from} to #{to} using int. #{interval}"
85
91
  logger.debug " TIME COST #{Time.now - t}"
86
92
  return axis
@@ -89,6 +95,8 @@ class GraphAxis
89
95
  (0...count).each do |i|
90
96
  axis << from + (l.to_f * i.to_f) / count.to_f
91
97
  end
98
+ axis = move_axis_to_fit_zero(axis) if adjust_axis_to_zero
99
+
92
100
  logger.debug "fixed count axis calculation from #{from} to #{to} using count #{count}"
93
101
  logger.debug " TIME COST #{Time.now - t}"
94
102
  return axis
@@ -96,6 +104,25 @@ class GraphAxis
96
104
  end
97
105
  end
98
106
 
107
+ # Process axis array, give offset to match zero axis, and remove zero axis from array
108
+ def move_axis_to_fit_zero(axis)
109
+ # if zero axis is within
110
+ if axis.min <= 0 and axis.max >= 0
111
+ # if there is axis within -1..1
112
+ axis.each_with_index do |a, i|
113
+ if a >= -1.0 and a <= 1.0
114
+ # this is the offset, move using found offset
115
+ return axis.collect { |b| b - a }
116
+ end
117
+ end
118
+ end
119
+
120
+ # TODO when it won't work?
121
+
122
+ # return unmodified
123
+ return axis
124
+ end
125
+
99
126
  # Enlarge image to maintain proper axis density
100
127
  def axis_distance_image_enlarge
101
128
  if options[:axis_density_enlarge_image]
@@ -112,6 +139,7 @@ class GraphAxis
112
139
  def x_axis_distance_image_enlarge
113
140
  a = parameter_axis
114
141
  # must be at least 2 axis
142
+ logger.debug "axis enlargement - parameter_axis #{a.inspect}"
115
143
  return if a.size < 2
116
144
 
117
145
  ax = a[0]
@@ -121,10 +149,12 @@ class GraphAxis
121
149
 
122
150
  axis_distance = (bx - ax).abs
123
151
 
152
+ logger.debug "axis enlargement - width, axis distance #{axis_distance} should be at least #{options[:x_axis_min_distance]}"
124
153
  if axis_distance < options[:x_axis_min_distance]
125
154
  # enlarging image
126
155
  options[:old_width] = options[:width]
127
156
  options[:width] *= (options[:x_axis_min_distance] / axis_distance).ceil
157
+ logger.debug "axis enlarged - width modified to #{options[:width]}"
128
158
  end
129
159
  end
130
160
 
@@ -132,6 +162,7 @@ class GraphAxis
132
162
  def y_axis_distance_image_enlarge
133
163
  a = value_axis
134
164
  # must be at least 2 axis
165
+ logger.debug "axis enlargement - value_axis #{a.inspect}"
135
166
  return if a.size < 2
136
167
 
137
168
  ay = a[0]
@@ -141,10 +172,12 @@ class GraphAxis
141
172
 
142
173
  axis_distance = (by - ay).abs
143
174
 
175
+ logger.debug "axis enlargement - height, axis distance #{axis_distance} should be at least #{options[:y_axis_min_distance]}"
144
176
  if axis_distance < options[:y_axis_min_distance]
145
177
  # enlarging image
146
178
  options[:old_height] = options[:height]
147
179
  options[:height] *= (options[:y_axis_min_distance] / axis_distance).ceil
180
+ logger.debug "axis enlarged - height modified from #{options[:old_height]} to #{options[:height]}"
148
181
  end
149
182
  end
150
183
 
@@ -42,6 +42,11 @@ class GraphColorLibrary
42
42
  FAIL_COLOR = 'black'
43
43
 
44
44
  def initialize
45
+ reset
46
+ end
47
+
48
+ # Reset color bank
49
+ def reset
45
50
  @colors = BASIC_COLORS + ADDITIONAL_COLORS.sort { rand }
46
51
  end
47
52
 
@@ -48,7 +48,7 @@ class GraphDataProcessor
48
48
  options[:y_axis_fixed_interval] = true if options[:y_axis_fixed_interval].nil?
49
49
 
50
50
  # when set enlarge image so axis are located in sensible distance between themselves
51
- options[:axis_density_enlarge_image] = false if options[:x_axis_fixed_interval].nil?
51
+ options[:axis_density_enlarge_image] = false if options[:axis_density_enlarge_image].nil?
52
52
  # distance in pixels
53
53
  options[:x_axis_min_distance] ||= 30
54
54
  # distance in pixels
@@ -17,15 +17,24 @@ class GraphImageDrawer
17
17
 
18
18
  # Which type of drawing class use?
19
19
  def drawing_class
20
+ if options[:drawer_class] == :rmagick and rmagick_installed?
21
+ require 'technical_graph/graph_image_drawer_rmagick'
22
+ return GraphImageDrawerRmagick
23
+ end
24
+
20
25
  if options[:drawer_class] == :rasem
21
26
  require 'technical_graph/graph_image_drawer_rasem'
22
27
  return GraphImageDrawerRasem
23
28
  end
24
29
 
25
- if options[:drawer_class] == :rmagick
26
- require 'technical_graph/graph_image_drawer_rmagick'
27
- return GraphImageDrawerRmagick
28
- end
30
+ # default
31
+ require 'technical_graph/graph_image_drawer_rasem'
32
+ return GraphImageDrawerRasem
33
+ end
34
+
35
+ # Check if rmagick is installed
36
+ def rmagick_installed?
37
+ return Gem.source_index.find_name('rmagick').size > 0
29
38
  end
30
39
 
31
40
  # Best output image format, used for testing
@@ -87,6 +96,8 @@ class GraphImageDrawer
87
96
  options[:axis_value_and_param_labels] = true if options[:axis_value_and_param_labels].nil?
88
97
  options[:axis_zero_labels] = true if options[:axis_zero_labels].nil?
89
98
 
99
+ options[:adjust_axis_to_zero] = true if options[:adjust_axis_to_zero].nil?
100
+
90
101
  # colors
91
102
  options[:background_color] ||= 'white'
92
103
  options[:background_hatch_color] ||= 'lightcyan2'
@@ -99,6 +110,7 @@ class GraphImageDrawer
99
110
  options[:axis_font_size] ||= 10
100
111
  options[:layers_font_size] ||= 10
101
112
  options[:axis_label_font_size] ||= 10
113
+ options[:legend_font_size] ||= 10
102
114
 
103
115
  # legend
104
116
  options[:legend] = false if options[:legend].nil?
@@ -155,6 +167,10 @@ class GraphImageDrawer
155
167
  options[:legend_width]
156
168
  end
157
169
 
170
+ def legend_height
171
+ options[:legend_height]
172
+ end
173
+
158
174
  def legend_margin
159
175
  options[:legend_margin]
160
176
  end
@@ -182,6 +198,9 @@ class GraphImageDrawer
182
198
 
183
199
  # Create background image
184
200
  def crate_blank_graph_image
201
+ # reset color banks
202
+ GraphColorLibrary.instance.reset
203
+ # calculate some stuff :]
185
204
  pre_image_create_calculations
186
205
  # create drawing proxy
187
206
  @drawer = drawing_class.new(self)
@@ -231,16 +250,28 @@ class GraphImageDrawer
231
250
  end
232
251
  end
233
252
 
234
- # height of 1 layer
235
- ONE_LAYER_LEGEND_HEIGHT = 15
253
+ # height of 1 layer without font size
254
+ ONE_LAYER_LEGEND_SPACER = 5
255
+
256
+ def one_layer_legend_height
257
+ options[:legend_font_size] + ONE_LAYER_LEGEND_SPACER
258
+ end
259
+
260
+ # Enlarge legend's width using legend labels sizes
261
+ def recalculate_legend_size
262
+ layers.each do |l|
263
+ w = l.label.size * options[:legend_font_size]
264
+ options[:legend_width] = w if w > legend_width
265
+ end
266
+
267
+ options[:legend_height] = layers.size * one_layer_legend_height
268
+ end
236
269
 
237
270
  # Choose best location
238
271
  def recalculate_legend_position
239
272
  return unless legend_auto_position
240
273
  logger.debug "Auto position calculation, drawn points #{@drawn_points.size}"
241
274
 
242
- legend_height = layers.size * ONE_LAYER_LEGEND_HEIGHT
243
-
244
275
  # check 8 places:
245
276
  positions = [
246
277
  { :x => legend_margin, :y => 0 + legend_margin }, # top-left
@@ -285,6 +316,7 @@ class GraphImageDrawer
285
316
  # Render legend on graph
286
317
  def render_data_legend
287
318
  return unless draw_legend?
319
+ recalculate_legend_size
288
320
  recalculate_legend_position
289
321
 
290
322
  x = legend_x
@@ -300,7 +332,7 @@ class GraphImageDrawer
300
332
  h[:y] = y
301
333
 
302
334
  legend_data << h
303
- y += ONE_LAYER_LEGEND_HEIGHT
335
+ y += one_layer_legend_height
304
336
  end
305
337
 
306
338
  drawer.legend(legend_data)
@@ -24,8 +24,16 @@ class GraphImageDrawerRasem
24
24
  @image.group :stroke => _options[:color], :stroke_width => _options[:width] do
25
25
  x_array.each_with_index do |x, i|
26
26
  line(x, 0, x, _s.height, { })
27
+ end
27
28
 
28
- # labels
29
+ y_array.each_with_index do |y, i|
30
+ line(0, y, _s.width, y, { })
31
+ end
32
+ end
33
+
34
+ # labels
35
+ @image.group :fill => _options[:color] do
36
+ x_array.each_with_index do |x, i|
29
37
  label = x_labels[i]
30
38
  if render_labels and not label.nil?
31
39
  label = "#{_s.truncate_string % label}"
@@ -34,8 +42,6 @@ class GraphImageDrawerRasem
34
42
  end
35
43
 
36
44
  y_array.each_with_index do |y, i|
37
- line(0, y, _s.width, y, { })
38
-
39
45
  # labels
40
46
  label = y_labels[i]
41
47
  if render_labels and not label.nil?
@@ -49,7 +55,7 @@ class GraphImageDrawerRasem
49
55
  # Label for parameters and values
50
56
  def axis_labels(parameter_label, value_label, _options = { :color => 'black', :width => 1, :size => 20 })
51
57
  _s = self
52
- @image.group :stroke => _options[:color], :stroke_width => _options[:width] do
58
+ @image.group :fill => _options[:color] do
53
59
  text(
54
60
  (_s.width / 2).to_i,
55
61
  _s.height - 40,
@@ -73,12 +79,12 @@ class GraphImageDrawerRasem
73
79
  if l.value_labels
74
80
  t = Time.now
75
81
 
76
- @image.group :stroke => _s.options[:axis_color], :stroke_width => 1 do
82
+ @image.group :fill => _s.options[:axis_color] do
77
83
  _coords.each do |c|
78
84
  string_label = "#{_s.truncate_string % c[:dy]}"
79
85
  text(
80
86
  c[:ax] + 5, c[:ay],
81
- string_label
87
+ string_label, {}
82
88
  )
83
89
  end
84
90
  end
@@ -113,11 +119,12 @@ class GraphImageDrawerRasem
113
119
 
114
120
  def legend(legend_data)
115
121
  _s = self
122
+ legend_text_offset = (options[:legend_font_size] / 2.0).round - 4
116
123
 
117
- @image.group :stroke_width => 1, :stroke => '' do
124
+ @image.group do
118
125
  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] })
126
+ circle(l[:x], l[:y], 2, { :stroke => l[:color], :fill => l[:color], :stroke_width => 1 })
127
+ text(l[:x] + 5, l[:y] + legend_text_offset, l[:label], { :fill => l[:color], 'font-size' => "#{_s.options[:legend_font_size]}px" })
121
128
  end
122
129
  end
123
130
  end
@@ -145,6 +152,8 @@ class GraphImageDrawerRasem
145
152
  else
146
153
  # ugly hack, save to svg and then convert using image magick
147
154
  tmp_file = file.gsub(/#{format}/, 'svg')
155
+ # change temp filename if it exist
156
+ tmp_file = File.join(Dir.tmpdir, "#{random_filename}.svg") if File.exists?(tmp_file)
148
157
  # save to svg
149
158
  save(tmp_file)
150
159
  # convert