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/DOCUMENTATION.md +6 -3
- data/DOCUMENTATION.textile +411 -16
- data/Gemfile +6 -1
- data/Gemfile.lock +13 -11
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/lib/technical_graph/data_layer.rb +1 -1
- data/lib/technical_graph/data_layer_processor_noise_removal.rb +46 -10
- data/lib/technical_graph/graph_axis.rb +33 -0
- data/lib/technical_graph/graph_color_library.rb +5 -0
- data/lib/technical_graph/graph_data_processor.rb +1 -1
- data/lib/technical_graph/graph_image_drawer.rb +41 -9
- data/lib/technical_graph/graph_image_drawer_rasem.rb +18 -9
- data/lib/technical_graph/graph_image_drawer_rmagick.rb +4 -1
- data/lib/technical_graph.rb +3 -2
- data/test/test_technical_graph_axis.rb +3 -1
- data/test/test_technical_readme.rb +305 -8
- metadata +7 -7
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
|
-
|
10
|
+
json (1.6.1)
|
11
|
+
rake (0.9.2.2)
|
11
12
|
rasem (0.6.1)
|
12
|
-
rcov (0.9.
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
rspec-
|
17
|
-
rspec-
|
18
|
-
|
19
|
-
rspec-
|
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.
|
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
|
-
|
34
|
+
rdoc
|
33
35
|
rspec
|
34
36
|
shoulda
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
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
|
-
#
|
60
|
-
|
61
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
|
@@ -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[:
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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 +=
|
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
|
-
|
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 :
|
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 :
|
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
|
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], { :
|
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
|