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/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
|