technical_graph 0.4.0 → 0.5.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.
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