technical_graph 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
@@ -0,0 +1,8 @@
1
+ <component name="ProjectDictionaryState">
2
+ <dictionary name="olek">
3
+ <words>
4
+ <w>antialias</w>
5
+ <w>magick</w>
6
+ </words>
7
+ </dictionary>
8
+ </component>
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.8.7@technical_graph --create
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'rmagick'
4
+ gem 'jeweler'
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "shoulda"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "rspec"
12
+ gem "jeweler" #, "~> 1.6.4"
13
+ gem "rcov", ">= 0"
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,32 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.3)
5
+ git (1.2.5)
6
+ jeweler (1.6.4)
7
+ bundler (~> 1.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rake (0.9.2)
11
+ rcov (0.9.10)
12
+ rmagick (2.13.1)
13
+ rspec (2.6.0)
14
+ rspec-core (~> 2.6.0)
15
+ rspec-expectations (~> 2.6.0)
16
+ rspec-mocks (~> 2.6.0)
17
+ rspec-core (2.6.4)
18
+ rspec-expectations (2.6.0)
19
+ diff-lcs (~> 1.1.2)
20
+ rspec-mocks (2.6.0)
21
+ shoulda (2.11.3)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ bundler (~> 1.0.0)
28
+ jeweler
29
+ rcov
30
+ rmagick
31
+ rspec
32
+ shoulda
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Aleksander Kwiatkowski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,19 @@
1
+ = technical-graph
2
+
3
+ Create neat graphs.
4
+
5
+ == Contributing to technical-graph
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2011 Aleksander Kwiatkowski. See LICENSE.txt for
18
+ further details.
19
+
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "technical_graph"
18
+ gem.homepage = "http://github.com/akwiatkowski/technical_graph"
19
+ gem.license = "LGPLv3"
20
+ gem.summary = %Q{Create simple and neat graphs}
21
+ gem.description = %Q{Create simple and neat graphs}
22
+ gem.email = "bobikx@poczta.fm"
23
+ gem.authors = ["Aleksander Kwiatkowski"]
24
+
25
+ #gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore']
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rake/testtask'
31
+ Rake::TestTask.new(:test) do |test|
32
+ test.libs << 'lib' << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+
37
+ require 'rcov/rcovtask'
38
+ Rcov::RcovTask.new do |test|
39
+ test.libs << 'test'
40
+ test.pattern = 'test/**/test_*.rb'
41
+ test.verbose = true
42
+ test.rcov_opts << '--exclude "gems/*"'
43
+ end
44
+
45
+ task :default => :test
46
+
47
+ require 'rake/rdoctask'
48
+ Rake::RDocTask.new do |rdoc|
49
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "technical-graph #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
@@ -0,0 +1,87 @@
1
+ #encoding: utf-8
2
+
3
+ # Stores only data used for one layer
4
+ # Instances of this class are used elsewhere
5
+ # Stores also drawing parameters for one layer
6
+
7
+ class DataLayer
8
+
9
+ def initialize(d = [], options = { })
10
+ @options = options
11
+ @data_params = Hash.new
12
+
13
+ # set data and append initial data
14
+ clear_data
15
+ append_data(d)
16
+ end
17
+
18
+ # Accessor for setting chart data for layer to draw
19
+ def append_data(d)
20
+ if d.kind_of? Array
21
+ @data += d
22
+ # sort, clean bad records
23
+ process_data
24
+ else
25
+ raise 'Data not an Array'
26
+ end
27
+ end
28
+
29
+ # Array of {:x =>, :y =>}
30
+ attr_reader :data
31
+
32
+ # Additional parameters
33
+ attr_reader :data_params
34
+
35
+ # Color of
36
+ def color
37
+ return @data_params[:color] || 'blue'
38
+ end
39
+
40
+ def antialias
41
+ return @data_params[:antialias] == false
42
+ end
43
+
44
+ # Clear data
45
+ def clear_data
46
+ @data = Array.new
47
+ end
48
+
49
+ # Clean and process data used for drawing current data layer
50
+ def process_data
51
+ # delete duplicates
52
+ @data = @data.inject([]) { |result, d| result << d unless result.select { |r| r[:x] == d[:x] }.size > 0; result }
53
+
54
+ @data.delete_if { |d| d[:x].nil? or d[:y].nil? }
55
+ @data.sort! { |d, e| d[:x] <=> e[:x] }
56
+
57
+ @data_params = Hash.new
58
+ # default X values, if data is not empty
59
+ if @data.size > 0
60
+ @data_params[:x_min] = @data.first[:x] || @options[:default_x_min]
61
+ @data_params[:x_max] = @data.last[:x] || @options[:default_x_max]
62
+
63
+ # default Y values
64
+ y_sort = @data.sort { |a, b| a[:y] <=> b[:y] }
65
+ @data_params[:y_min] = y_sort.first[:y] || @options[:default_y_min]
66
+ @data_params[:y_max] = y_sort.last[:y] || @options[:@default_y_max]
67
+ end
68
+
69
+ end
70
+
71
+ def x_min
72
+ @data_params[:x_min]
73
+ end
74
+
75
+ def x_max
76
+ @data_params[:x_max]
77
+ end
78
+
79
+ def y_min
80
+ @data_params[:y_min]
81
+ end
82
+
83
+ def y_max
84
+ @data_params[:y_max]
85
+ end
86
+
87
+ end
@@ -0,0 +1,248 @@
1
+ #encoding: utf-8
2
+
3
+ # Calculate axis (only) and draw them
4
+
5
+ class GraphAxis
6
+
7
+ attr_reader :technical_graph
8
+
9
+ # Accessor for options Hash
10
+ def options
11
+ @technical_graph.options
12
+ end
13
+
14
+ # Accessor for DataLayer Array
15
+ def layers
16
+ @technical_graph.layers
17
+ end
18
+
19
+ # Calculate everything
20
+ def data_processor
21
+ @technical_graph.data_processor
22
+ end
23
+
24
+ def image_drawer
25
+ @technical_graph.image_drawer
26
+ end
27
+
28
+ def initialize(technical_graph)
29
+ @technical_graph = technical_graph
30
+ end
31
+
32
+ def x_axis_fixed?
33
+ options[:x_axises_fixed_interval] == true
34
+ end
35
+
36
+ # Value axis has fixed count
37
+ def y_axis_fixed?
38
+ options[:y_axises_fixed_interval] == true
39
+ end
40
+
41
+ # Where to put axis values
42
+ def value_axises
43
+ return calc_axis(data_processor.y_min, data_processor.y_max, options[:y_axises_interval], options[:y_axises_count], y_axis_fixed?)
44
+ end
45
+
46
+ # Where to put axis values
47
+ def parameter_axises
48
+ return calc_axis(data_processor.x_min, data_processor.x_max, options[:x_axises_interval], options[:x_axises_count], x_axis_fixed?)
49
+ end
50
+
51
+ # Calculate axis using 2 methods
52
+ def calc_axis(from, to, interval, count, fixed_interval)
53
+ axises = Array.new
54
+ l = to - from
55
+ current = from
56
+
57
+ if fixed_interval
58
+ while current < to
59
+ axises << current
60
+ current += interval
61
+ end
62
+ return axises
63
+
64
+ else
65
+ (0...count).each do |i|
66
+ axises << from + (l.to_f * i.to_f) / count.to_f
67
+ end
68
+ return axises
69
+
70
+ end
71
+ end
72
+
73
+
74
+ # Render axis on image
75
+ def render_on_image(image)
76
+ @image = image
77
+
78
+ render_values_axis
79
+ render_parameters_axis
80
+
81
+ render_values_zero_axis
82
+ render_parameters_zero_axis
83
+ end
84
+
85
+ def axis_antialias
86
+ options[:axis_antialias] == true
87
+ end
88
+
89
+
90
+ def render_values_axis
91
+ plot_axis_y_line = Magick::Draw.new
92
+ plot_axis_y_text = Magick::Draw.new
93
+
94
+ plot_axis_y_line.stroke_antialias(axis_antialias)
95
+ plot_axis_y_text.text_antialias(image_drawer.font_antialias)
96
+
97
+ plot_axis_y_line.fill_opacity(0)
98
+ plot_axis_y_line.stroke(options[:axis_color])
99
+ plot_axis_y_line.stroke_opacity(1.0)
100
+ plot_axis_y_line.stroke_width(1.0)
101
+ plot_axis_y_line.stroke_linecap('square')
102
+ plot_axis_y_line.stroke_linejoin('miter')
103
+
104
+ plot_axis_y_text.font_family('helvetica')
105
+ plot_axis_y_text.font_style(Magick::NormalStyle)
106
+ plot_axis_y_text.text_align(Magick::LeftAlign)
107
+ plot_axis_y_text.text_undercolor(options[:background_color])
108
+
109
+ value_axises.each do |y|
110
+ by = image_drawer.calc_bitmap_y(y)
111
+ plot_axis_y_line.line(
112
+ 0, by.round,
113
+ @image.columns-1, by.round
114
+ )
115
+
116
+ plot_axis_y_text.text(
117
+ 5,
118
+ by.round + 15,
119
+ "#{y}"
120
+ )
121
+ end
122
+
123
+ t = Time.now
124
+ plot_axis_y_line.draw(@image)
125
+ puts "#{Time.now - t} drawing lines"
126
+ plot_axis_y_text.draw(@image)
127
+ puts "#{Time.now - t} drawing text"
128
+ end
129
+
130
+ def render_parameters_axis
131
+
132
+ plot_axis_x_line = Magick::Draw.new
133
+ plot_axis_x_text = Magick::Draw.new
134
+
135
+ plot_axis_x_line.stroke_antialias(axis_antialias)
136
+ plot_axis_x_text.text_antialias(axis_antialias)
137
+
138
+ plot_axis_x_line.fill_opacity(0)
139
+ plot_axis_x_line.stroke(options[:axis_color])
140
+ plot_axis_x_line.stroke_opacity(1.0)
141
+ plot_axis_x_line.stroke_width(1.0)
142
+ plot_axis_x_line.stroke_linecap('square')
143
+ plot_axis_x_line.stroke_linejoin('miter')
144
+
145
+ plot_axis_x_text.font_family('helvetica')
146
+ plot_axis_x_text.font_style(Magick::NormalStyle)
147
+ plot_axis_x_text.text_align(Magick::LeftAlign)
148
+ plot_axis_x_text.text_undercolor(options[:background_color])
149
+
150
+ parameter_axises.each do |x|
151
+ bx = image_drawer.calc_bitmap_x(x)
152
+ plot_axis_x_line.line(
153
+ bx.round, 0,
154
+ bx.round, @image.rows-1
155
+ )
156
+
157
+ plot_axis_x_text.text(
158
+ bx.round + 15,
159
+ @image.rows - 15,
160
+ "#{x}"
161
+ )
162
+ end
163
+
164
+ t = Time.now
165
+ plot_axis_x_line.draw(@image)
166
+ puts "#{Time.now - t} drawing lines"
167
+ plot_axis_x_text.draw(@image)
168
+ puts "#{Time.now - t} drawing text"
169
+
170
+ end
171
+
172
+ # TODO: make it DRY
173
+ def render_values_zero_axis
174
+ plot_axis_y_line = Magick::Draw.new
175
+ plot_axis_y_text = Magick::Draw.new
176
+
177
+ plot_axis_y_line.stroke_antialias(axis_antialias)
178
+ plot_axis_y_text.text_antialias(image_drawer.font_antialias)
179
+
180
+ plot_axis_y_line.fill_opacity(0)
181
+ plot_axis_y_line.stroke(options[:axis_color])
182
+ plot_axis_y_line.stroke_opacity(1.0)
183
+ plot_axis_y_line.stroke_width(2.0)
184
+ plot_axis_y_line.stroke_linecap('square')
185
+ plot_axis_y_line.stroke_linejoin('miter')
186
+
187
+ plot_axis_y_text.font_family('helvetica')
188
+ plot_axis_y_text.font_style(Magick::NormalStyle)
189
+ plot_axis_y_text.text_align(Magick::LeftAlign)
190
+ plot_axis_y_text.text_undercolor(options[:background_color])
191
+
192
+ y = 0.0
193
+ by = image_drawer.calc_bitmap_y(y)
194
+ plot_axis_y_line.line(
195
+ 0, by.round,
196
+ @image.columns-1, by.round
197
+ )
198
+
199
+ plot_axis_y_text.text(
200
+ 5,
201
+ by.round + 15,
202
+ "#{y}"
203
+ )
204
+
205
+ # TODO: why normal axis does not need it?
206
+ plot_axis_y_line.draw(@image)
207
+ plot_axis_y_text.draw(@image)
208
+ end
209
+
210
+ def render_parameters_zero_axis
211
+
212
+ plot_axis_x_line = Magick::Draw.new
213
+ plot_axis_x_text = Magick::Draw.new
214
+
215
+ plot_axis_x_line.stroke_antialias(axis_antialias)
216
+ plot_axis_x_text.text_antialias(axis_antialias)
217
+
218
+ plot_axis_x_line.fill_opacity(0)
219
+ plot_axis_x_line.stroke(options[:axis_color])
220
+ plot_axis_x_line.stroke_opacity(1.0)
221
+ plot_axis_x_line.stroke_width(2.0)
222
+ plot_axis_x_line.stroke_linecap('square')
223
+ plot_axis_x_line.stroke_linejoin('miter')
224
+
225
+ plot_axis_x_text.font_family('helvetica')
226
+ plot_axis_x_text.font_style(Magick::NormalStyle)
227
+ plot_axis_x_text.text_align(Magick::LeftAlign)
228
+ plot_axis_x_text.text_undercolor(options[:background_color])
229
+
230
+ x = 0.0
231
+ bx = image_drawer.calc_bitmap_x(x)
232
+ plot_axis_x_line.line(
233
+ bx.round, 0,
234
+ bx.round, @image.rows-1
235
+ )
236
+
237
+ plot_axis_x_text.text(
238
+ bx.round + 15,
239
+ @image.rows - 15,
240
+ "#{x}"
241
+ )
242
+
243
+ # TODO: why normal axis does not need it?
244
+ plot_axis_x_line.draw(@image)
245
+ plot_axis_x_text.draw(@image)
246
+ end
247
+
248
+ end
@@ -0,0 +1,175 @@
1
+ #encoding: utf-8
2
+
3
+ require 'technical_graph/data_layer'
4
+
5
+ # Calculate every aspect of graph, but not directly image oriented variables
6
+
7
+ class GraphDataProcessor
8
+ attr_reader :technical_graph
9
+
10
+ # Accessor for options Hash
11
+ def options
12
+ @technical_graph.options
13
+ end
14
+
15
+ # Accessor for DataLayer Array
16
+ def layers
17
+ @technical_graph.layers
18
+ end
19
+
20
+ def initialize(technical_graph)
21
+ @technical_graph = technical_graph
22
+
23
+ options[:x_min] ||= (Time.now - 24 * 3600).to_f
24
+ options[:x_max] ||= Time.now.to_f
25
+ options[:y_min] ||= 0.0
26
+ options[:y_max] ||= 1.0
27
+ # :default - coords are default
28
+ # :fixed or whatever else - min/max coords are fixed
29
+ options[:xy_behaviour] ||= :default
30
+
31
+ # number of axises
32
+ options[:y_axises_count] ||= 10
33
+ options[:x_axises_count] ||= 10
34
+ # interval
35
+ options[:y_axises_interval] ||= 1.0
36
+ options[:x_axises_interval] ||= 1.0
37
+ # when false then axises are generated to meet 'count'
38
+ # when true then axises are generated every X from lowest
39
+ options[:x_axises_fixed_interval] = true if options[:x_axises_fixed_interval].nil?
40
+ options[:y_axises_fixed_interval] = true if options[:y_axises_fixed_interval].nil?
41
+
42
+ # default truncate string used for rendering numbers
43
+ options[:truncate_string] ||= "%.2f"
44
+
45
+ @zoom_x = 1.0
46
+ @zoom_y = 1.0
47
+ end
48
+
49
+ # Ranges are fixed
50
+ def fixed?
51
+ options[:xy_behaviour] == :fixed
52
+ end
53
+
54
+ # Ranges without zoom
55
+ def raw_x_min
56
+ options[:x_min]
57
+ end
58
+
59
+ def raw_x_max
60
+ options[:x_max]
61
+ end
62
+
63
+ def raw_y_min
64
+ options[:y_min]
65
+ end
66
+
67
+ def raw_y_max
68
+ options[:y_max]
69
+ end
70
+
71
+ # Ranges with zoom
72
+ def x_min
73
+ calc_x_zoomed([self.raw_x_min]).first
74
+ end
75
+
76
+ def x_max
77
+ calc_x_zoomed([self.raw_x_max]).first
78
+ end
79
+
80
+ def y_min
81
+ calc_y_zoomed([self.raw_y_min]).first
82
+ end
83
+
84
+ def y_max
85
+ calc_y_zoomed([self.raw_y_max]).first
86
+ end
87
+
88
+ # Accessors
89
+ private
90
+ def raw_x_min=(x)
91
+ options[:x_min] = x
92
+ end
93
+
94
+ def raw_x_max=(x)
95
+ options[:x_max] = x
96
+ end
97
+
98
+ def raw_y_min=(y)
99
+ options[:y_min] = y
100
+ end
101
+
102
+ def raw_y_max=(y)
103
+ options[:y_max] = y
104
+ end
105
+
106
+ public
107
+
108
+ # Consider changing ranges if needed
109
+ def process_data_layer(data_layer)
110
+ # ranges are set, can't change
111
+ return if fixed?
112
+
113
+ # updating ranges
114
+ self.raw_y_max = data_layer.y_max if not data_layer.y_max.nil? and data_layer.y_max > self.raw_y_max
115
+ self.raw_x_max = data_layer.x_max if not data_layer.x_max.nil? and data_layer.x_max > self.raw_x_max
116
+
117
+ self.raw_y_min = data_layer.y_min if not data_layer.y_min.nil? and data_layer.y_min < self.raw_y_min
118
+ self.raw_x_min = data_layer.x_min if not data_layer.x_min.nil? and data_layer.x_min < self.raw_x_min
119
+ end
120
+
121
+ # Change overall image zoom
122
+ def zoom=(z = 1.0)
123
+ self.x_zoom = z
124
+ self.y_zoom = z
125
+ end
126
+
127
+ # Change X axis zoom
128
+ def x_zoom=(z = 1.0)
129
+ @zoom_x = z
130
+ end
131
+
132
+ # Change X axis zoom
133
+ def y_zoom=(z = 1.0)
134
+ @zoom_y = z
135
+ end
136
+
137
+ attr_reader :zoom_x, :zoom_y
138
+
139
+ # Calculate zoomed X position for Array of X'es
140
+ def calc_x_zoomed(old_xes)
141
+ # default zoom
142
+ if self.zoom_x == 1.0
143
+ return old_xes
144
+ end
145
+
146
+ a = (raw_x_max.to_f + raw_x_min.to_f) / 2.0
147
+ new_xes = Array.new
148
+
149
+ old_xes.each do |x|
150
+ d = x - a
151
+ new_xes << (a + d * self.zoom_x)
152
+ end
153
+
154
+ return new_xes
155
+ end
156
+
157
+ # Calculate zoomed Y position for Array of Y'es
158
+ def calc_y_zoomed(old_yes)
159
+ # default zoom
160
+ if self.zoom_y == 1.0
161
+ return old_yes
162
+ end
163
+
164
+ a = (raw_y_max.to_f + raw_y_min.to_f) / 2.0
165
+ new_yes = Array.new
166
+
167
+ old_yes.each do |y|
168
+ d = y - a
169
+ new_yes << (a + d * self.zoom_y)
170
+ end
171
+
172
+ return new_yes
173
+ end
174
+
175
+ end