technical_graph 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,190 @@
1
+ #encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'RMagick'
5
+ require 'date'
6
+
7
+ # Universal class for creating graphs/charts.
8
+
9
+ # options parameters:
10
+ # :width - width of image
11
+ # :height - height of image
12
+ # :x_min, :x_max, :y_min, :y_max - default or fixed ranges
13
+ # :xy_behaviour:
14
+ # * :default - use them as default ranges
15
+ # * :fixed - ranges will not be changed during addition of layers
16
+
17
+ class GraphImageDrawer
18
+
19
+ attr_reader :technical_graph
20
+
21
+ # Accessor for options Hash
22
+ def options
23
+ @technical_graph.options
24
+ end
25
+
26
+ # Accessor for DataLayer Array
27
+ def layers
28
+ @technical_graph.layers
29
+ end
30
+
31
+ # Calculate everything
32
+ def data_processor
33
+ @technical_graph.data_processor
34
+ end
35
+
36
+ def truncate_string
37
+ options[:truncate_string]
38
+ end
39
+
40
+
41
+ # default sizes
42
+ DEFAULT_WIDTH = 1600
43
+ DEFAULT_HEIGHT = 1200
44
+
45
+ def initialize(technical_graph)
46
+ @technical_graph = technical_graph
47
+
48
+ options[:width] ||= DEFAULT_WIDTH
49
+ options[:height] ||= DEFAULT_HEIGHT
50
+
51
+ # colors
52
+ options[:background_color] ||= 'white'
53
+ options[:background_hatch_color] ||= 'lightcyan2'
54
+ options[:axis_color] ||= '#aaaaaa'
55
+ end
56
+
57
+ def width
58
+ options[:width].to_i
59
+ end
60
+
61
+ def height
62
+ options[:height].to_i
63
+ end
64
+
65
+ def width=(w)
66
+ options[:width] = w.to_i if w.to_i > 0
67
+ end
68
+
69
+ def height=(h)
70
+ options[:height] = h.to_i if h.to_i > 0
71
+ end
72
+
73
+ def font_antialias
74
+ options[:font_antialias] == true
75
+ end
76
+
77
+ # Calculate image X position
78
+ def calc_bitmap_x(_x)
79
+ l = data_processor.x_max - data_processor.x_min
80
+ offset = _x - data_processor.x_min
81
+ return (offset.to_f * width.to_f) / l.to_f
82
+ end
83
+
84
+ # Calculate image Y position
85
+ def calc_bitmap_y(_y)
86
+ l = data_processor.y_max - data_processor.y_min
87
+ #offset = _y - data_processor.y_min
88
+ offset = data_processor.y_max - _y
89
+ return (offset.to_f * height.to_f) / l.to_f
90
+ end
91
+
92
+ # Create background image
93
+ def crate_blank_graph_image
94
+ @image = Magick::ImageList.new
95
+ @image.new_image(
96
+ width,
97
+ height,
98
+ Magick::HatchFill.new(
99
+ options[:background_color],
100
+ options[:background_hatch_color]
101
+ )
102
+ )
103
+
104
+ return @image
105
+ end
106
+
107
+ attr_reader :image
108
+
109
+ # Render data layer
110
+ def render_data_layer(l)
111
+ layer_line = Magick::Draw.new
112
+ layer_text = Magick::Draw.new
113
+
114
+ layer_line.stroke_antialias(l.antialias)
115
+ layer_line.fill(l.color)
116
+ layer_line.fill_opacity(1)
117
+ layer_line.stroke(l.color)
118
+ layer_line.stroke_opacity(1.0)
119
+ layer_line.stroke_width(1.0)
120
+ layer_line.stroke_linecap('square')
121
+ layer_line.stroke_linejoin('miter')
122
+
123
+ layer_text.text_antialias(font_antialias)
124
+ layer_text.pointsize(10)
125
+ layer_text.font_family('helvetica')
126
+ layer_text.font_style(Magick::NormalStyle)
127
+ layer_text.text_align(Magick::LeftAlign)
128
+ layer_text.text_undercolor(options[:background_color])
129
+
130
+ # calculate coords, draw text, and then lines and circles
131
+ coords = Array.new
132
+
133
+ (0...(l.data.size - 1)).each do |i|
134
+ ax = l.data[i][:x]
135
+ ax = calc_bitmap_x(ax).round
136
+ ay = l.data[i][:y]
137
+ ay = calc_bitmap_y(ay).round
138
+
139
+ bx = l.data[i+1][:x]
140
+ bx = calc_bitmap_x(bx).round
141
+ by = l.data[i+1][:y]
142
+ by = calc_bitmap_y(by).round
143
+
144
+ coords << {
145
+ :ax => ax, :ay => ay,
146
+ :bx => bx, :by => by,
147
+ :dy => l.data[i][:y]
148
+ }
149
+ end
150
+
151
+ # labels
152
+ coords.each do |c|
153
+ string_label = "#{truncate_string % c[:dy]}"
154
+ layer_text.text(
155
+ c[:ax] + 5, c[:ay],
156
+ string_label
157
+ )
158
+ end
159
+ layer_text.draw(@image)
160
+
161
+ # lines and circles
162
+ coords.each do |c|
163
+ # additional circle
164
+ layer_line.circle(
165
+ c[:ax], c[:ay],
166
+ c[:ax] + 3, c[:ay]
167
+ )
168
+ layer_line.circle(
169
+ c[:bx], c[:by],
170
+ c[:bx] + 3, c[:by]
171
+ )
172
+
173
+ # line
174
+ layer_line.line(
175
+ c[:ax], c[:ay],
176
+ c[:bx], c[:by]
177
+ )
178
+ end
179
+ layer_line.draw(@image)
180
+
181
+ end
182
+
183
+ # Save output to file
184
+ def save_to_file(file)
185
+ @image.write(file)
186
+ end
187
+
188
+
189
+ end
190
+
@@ -0,0 +1,151 @@
1
+ #encoding: utf-8
2
+
3
+ require 'technical_graph/axis_layer_draw_module'
4
+
5
+ # Decide min/max values, recalculate all points and draw axises
6
+
7
+ class AxisLayer
8
+ include AxisLayerDrawModule
9
+
10
+ def initialize(options = { })
11
+ @options = options
12
+ @options[:x_min] ||= (Time.now - 24 * 3600).to_f
13
+ @options[:x_max] ||= Time.now.to_f
14
+ @options[:y_min] ||= 0.0
15
+ @options[:y_max] ||= 1.0
16
+ # :default - coords are default
17
+ # :fixed or whatever else - min/max coords are fixed
18
+ @options[:xy_behaviour] ||= :default
19
+
20
+ # number of axises
21
+ @options[:y_axises_count] ||= 10
22
+ @options[:x_axises_count] ||= 10
23
+ # interval
24
+ @options[:y_axises_interval] ||= 1.0
25
+ @options[:x_axises_interval] ||= 1.0
26
+ # when false then axises are generated to meet 'count'
27
+ # when true then axises are generated every X from lowest
28
+ @options[:x_axises_fixed_interval] = true if @options[:x_axises_fixed_interval].nil?
29
+ @options[:y_axises_fixed_interval] = true if @options[:y_axises_fixed_interval].nil?
30
+
31
+ @zoom_x = 1.0
32
+ @zoom_y = 1.0
33
+ end
34
+
35
+ # Ranges are fixed
36
+ def fixed?
37
+ @options[:xy_behaviour] == :fixed
38
+ end
39
+
40
+ # Ranges without zoom
41
+ def raw_x_min
42
+ @options[:x_min]
43
+ end
44
+
45
+ def raw_x_max
46
+ @options[:x_max]
47
+ end
48
+
49
+ def raw_y_min
50
+ @options[:y_min]
51
+ end
52
+
53
+ def raw_y_max
54
+ @options[:y_max]
55
+ end
56
+
57
+ # Ranges with zoom
58
+ def x_min
59
+ calc_x_zoomed([self.raw_x_min]).first
60
+ end
61
+
62
+ def x_max
63
+ calc_x_zoomed([self.raw_x_max]).first
64
+ end
65
+
66
+ def y_min
67
+ calc_y_zoomed([self.raw_y_min]).first
68
+ end
69
+
70
+ def y_max
71
+ calc_y_zoomed([self.raw_y_max]).first
72
+ end
73
+
74
+ # Accessors
75
+ private
76
+ def raw_x_min=(x)
77
+ @options[:x_min] = x
78
+ end
79
+
80
+ def raw_x_max=(x)
81
+ @options[:x_max] = x
82
+ end
83
+
84
+ def raw_y_min=(y)
85
+ @options[:y_min] = y
86
+ end
87
+
88
+ def raw_y_max=(y)
89
+ @options[:y_max] = y
90
+ end
91
+
92
+ public
93
+
94
+ # Consider changing ranges
95
+ def process_data_layer(data_layer)
96
+ # ranges are set, can't change sir
97
+ return if fixed?
98
+
99
+ # updating ranges
100
+ self.raw_y_max = data_layer.y_max if not data_layer.y_max.nil? and data_layer.y_max > self.raw_y_max
101
+ self.raw_x_max = data_layer.x_max if not data_layer.x_max.nil? and data_layer.x_max > self.raw_x_max
102
+
103
+ self.raw_y_min = data_layer.y_min if not data_layer.y_min.nil? and data_layer.y_min < self.raw_y_min
104
+ self.raw_x_min = data_layer.x_min if not data_layer.x_min.nil? and data_layer.x_min < self.raw_x_min
105
+ end
106
+
107
+ # Change overall image zoom
108
+ def zoom=(z = 1.0)
109
+ self.x_zoom = z
110
+ self.y_zoom = z
111
+ end
112
+
113
+ # Change X axis zoom
114
+ def x_zoom=(z = 1.0)
115
+ @zoom_x = z
116
+ end
117
+
118
+ # Change X axis zoom
119
+ def y_zoom=(z = 1.0)
120
+ @zoom_y = z
121
+ end
122
+
123
+ attr_reader :zoom_x, :zoom_y
124
+
125
+ # Calculate zoomed X position for Array of X'es
126
+ def calc_x_zoomed(old_xes)
127
+ a = (raw_x_max.to_f + raw_x_min.to_f) / 2.0
128
+ new_xes = Array.new
129
+
130
+ old_xes.each do |x|
131
+ d = x - a
132
+ new_xes << (a + d * self.zoom_x)
133
+ end
134
+
135
+ return new_xes
136
+ end
137
+
138
+ # Calculate zoomed Y position for Array of Y'es
139
+ def calc_y_zoomed(old_yes)
140
+ a = (raw_y_max.to_f + raw_y_min.to_f) / 2.0
141
+ new_yes = Array.new
142
+
143
+ old_yes.each do |y|
144
+ d = y - a
145
+ new_yes << (a + d * self.zoom_y)
146
+ end
147
+
148
+ return new_yes
149
+ end
150
+
151
+ end
@@ -0,0 +1,145 @@
1
+ module AxisLayerDrawModule
2
+ def x_axis_fixed?
3
+ @options[:x_axises_fixed_interval] == true
4
+ end
5
+
6
+ # Value axis has fixed count
7
+ def y_axis_fixed?
8
+ @options[:y_axises_fixed_interval] == true
9
+ end
10
+
11
+ # Where to put axis values
12
+ def value_axises
13
+ return calc_axis(self.y_min, self.y_max, @options[:y_axises_interval], @options[:y_axises_count], y_axis_fixed?)
14
+ end
15
+
16
+ # Where to put axis values
17
+ def parameter_axises
18
+ return calc_axis(self.x_min, self.x_max, @options[:x_axises_interval], @options[:x_axises_count], x_axis_fixed?)
19
+ end
20
+
21
+ # Calculate axis using 2 methods
22
+ def calc_axis(from, to, interval, count, fixed_interval)
23
+ axises = Array.new
24
+ l = to - from
25
+ current = from
26
+
27
+ if fixed_interval
28
+ while current < to
29
+ axises << current
30
+ current += interval
31
+ end
32
+ return axises
33
+
34
+ else
35
+ (0...count).each do |i|
36
+ axises << from + (l.to_f * i.to_f) / count.to_f
37
+ end
38
+ return axises
39
+
40
+ end
41
+ end
42
+
43
+
44
+ def calc_bitmap_position(array)
45
+ # TODO move calculatio of lenght here
46
+ end
47
+
48
+ def calc_bitmap_x(_x)
49
+ l = self.x_max - self.x_min
50
+ offset = _x - self.x_min
51
+ return (offset.to_f * @image.width.to_f) / l.to_f
52
+ end
53
+
54
+ def calc_bitmap_y(_y)
55
+ l = self.y_max - self.y_min
56
+ offset = _y - self.y_min
57
+ return (offset.to_f * @image.width.to_f) / l.to_f
58
+ end
59
+
60
+ # Render axis on image
61
+ def render_on_image(image)
62
+ @image = image
63
+
64
+ render_values_axis
65
+ render_parameters_axis
66
+ end
67
+
68
+ def render_values_axis
69
+ plot_axis_y_line = Magick::Draw.new
70
+ plot_axis_y_text = Magick::Draw.new
71
+
72
+ plot_axis_y_line.fill_opacity(0)
73
+ plot_axis_y_line.stroke(@image.options[:axis_color])
74
+ plot_axis_y_line.stroke_opacity(1.0)
75
+ plot_axis_y_line.stroke_width(1.0)
76
+ plot_axis_y_line.stroke_linecap('square')
77
+ plot_axis_y_line.stroke_linejoin('miter')
78
+
79
+ plot_axis_y_text.font_family('helvetica')
80
+ plot_axis_y_text.font_style(Magick::NormalStyle)
81
+ plot_axis_y_text.text_align(Magick::LeftAlign)
82
+ plot_axis_y_text.text_undercolor(@image.options[:background_color])
83
+
84
+ value_axises.each do |y|
85
+ by = calc_bitmap_y(y)
86
+ plot_axis_y_line.line(
87
+ 0, by.round,
88
+ @image.image.columns-1, by.round
89
+ )
90
+
91
+ plot_axis_y_text.text(
92
+ 5,
93
+ by.round + 15,
94
+ "#{y}"
95
+ )
96
+ end
97
+
98
+ t = Time.now
99
+ plot_axis_y_line.draw(@image.image)
100
+ puts "#{Time.now - t} drawing lines"
101
+ plot_axis_y_text.draw(@image.image)
102
+ puts "#{Time.now - t} drawing text"
103
+
104
+ end
105
+
106
+ def render_parameters_axis
107
+
108
+ plot_axis_x_line = Magick::Draw.new
109
+ plot_axis_x_text = Magick::Draw.new
110
+
111
+ plot_axis_x_line.fill_opacity(0)
112
+ plot_axis_x_line.stroke(@image.options[:axis_color])
113
+ plot_axis_x_line.stroke_opacity(1.0)
114
+ plot_axis_x_line.stroke_width(1.0)
115
+ plot_axis_x_line.stroke_linecap('square')
116
+ plot_axis_x_line.stroke_linejoin('miter')
117
+
118
+ plot_axis_x_text.font_family('helvetica')
119
+ plot_axis_x_text.font_style(Magick::NormalStyle)
120
+ plot_axis_x_text.text_align(Magick::LeftAlign)
121
+ plot_axis_x_text.text_undercolor(@image.options[:background_color])
122
+
123
+ parameter_axises.each do |x|
124
+ bx = calc_bitmap_x(x)
125
+ plot_axis_x_line.line(
126
+ bx.round, 0,
127
+ bx.round, @image.image.rows-1
128
+ )
129
+
130
+ plot_axis_x_text.text(
131
+ bx.round + 15,
132
+ @image.image.rows - 15,
133
+ "#{x}"
134
+ )
135
+ end
136
+
137
+ t = Time.now
138
+ plot_axis_x_line.draw(@image.image)
139
+ puts "#{Time.now - t} drawing lines"
140
+ plot_axis_x_text.draw(@image.image)
141
+ puts "#{Time.now - t} drawing text"
142
+
143
+ end
144
+
145
+ end
@@ -0,0 +1,55 @@
1
+ #encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'technical_graph/data_processor'
5
+ #require 'technical_graph/graph_image'
6
+ #require 'technical_graph/data_layer'
7
+ #require 'technical_graph/axis_layer'
8
+
9
+ # Universal class for creating graphs/charts.
10
+
11
+ # options parameters:
12
+ # :width - width of image
13
+ # :height - height of image
14
+ # :x_min, :x_max, :y_min, :y_max - default or fixed ranges
15
+ # :xy_behaviour:
16
+ # * :default - use them as default ranges
17
+ # * :fixed - ranges will not be changed during addition of layers
18
+
19
+ class TechnicalGraph
20
+
21
+ def initialize(options = { })
22
+ @options = options
23
+ @data_processor = GraphDataProcessor.new(@options)
24
+
25
+ #@image = GraphImage.new(@options)
26
+ #@axis = AxisLayer.new(@options)
27
+ #@layers = []
28
+ end
29
+ attr_reader :options
30
+ #attr_reader :image
31
+ #attr_reader :layers
32
+ #attr_reader :axis
33
+
34
+ # Add new data layer to layer array
35
+ def add_layer(data = [], options = {})
36
+ @layers << DataLayer.new(data, options)
37
+ end
38
+
39
+ # Create graph
40
+ def render
41
+ @image.render_image
42
+ # recalculate ranges
43
+ @layers.each do |l|
44
+ @axis.process_data_layer(l)
45
+ end
46
+ # draw axis
47
+ @axis.render_on_image(@image)
48
+ # draw layers
49
+ @layers.each do |l|
50
+ # @xis used for calculation purpose
51
+ l.render_on_image(@image, @axis)
52
+ end
53
+ end
54
+
55
+ end
@@ -0,0 +1,104 @@
1
+ require 'helper'
2
+
3
+ class TestTechnicalGraph < Test::Unit::TestCase
4
+ context 'initial options' do
5
+ setup do
6
+ @technical_graph = TechnicalGraph.new
7
+ end
8
+
9
+ should "has options with default values" do
10
+ @technical_graph.options.class.should == Hash
11
+ @technical_graph.options[:width] > 0
12
+ @technical_graph.options[:height] > 0
13
+ end
14
+
15
+ should "has options with custom values" do
16
+ s = 10
17
+ tg = TechnicalGraph.new({ :height => s, :width => s })
18
+ @technical_graph.options[:width] == s
19
+ @technical_graph.options[:height] == s
20
+
21
+ @technical_graph.image.width == s
22
+ @technical_graph.image.height == s
23
+ end
24
+
25
+ should "has changeable options" do
26
+ s = 20
27
+ tg = TechnicalGraph.new
28
+ tg.image.width = s
29
+ tg.image.height = s
30
+ @technical_graph.options[:width] == s
31
+ @technical_graph.options[:height] == s
32
+ end
33
+ end
34
+
35
+ context 'basic layer operation and saving file' do
36
+ setup do
37
+ @tg = TechnicalGraph.new
38
+ @data_size = 100
39
+
40
+ # sample data
41
+ @data = Array.new
42
+ @second_data = Array.new
43
+ (0...@data_size).each do |i|
44
+ @data << { :x => Time.now.to_i - 3600 + i, :y => Math.sin(i.to_f / 10.0) }
45
+ @second_data << { :x => Time.now.to_i - 1800 + i*2, :y => Math.cos(i.to_f / 10.0) }
46
+ end
47
+ end
48
+
49
+ should 'has ability do add new layer' do
50
+ layers = @tg.layers.size
51
+ @tg.add_layer(@data)
52
+ @tg.layers.size.should == layers + 1
53
+
54
+ layer = @tg.layers.last
55
+ layer.data.size.should == @data_size
56
+ end
57
+
58
+ should 'has ability to manipulate layers, add more data' do
59
+ @tg.add_layer(@data)
60
+ layer = @tg.layers.last
61
+ layer.class.should == DataLayer
62
+
63
+ layer.data.size.should == @data_size
64
+
65
+ # adding second data
66
+ layer.append_data(@second_data)
67
+ layer.data.size.should == 2 * @data_size
68
+
69
+ # @tg.render
70
+ # @tg.image.save_to_file('test1.png')
71
+ end
72
+
73
+ should 'has ability to filter records with similar x\'es' do
74
+ @tg.add_layer
75
+ layer = @tg.layers.last
76
+ layer.data.size.should == 0
77
+ layer.append_data([{ :x => 0, :y => 1 }])
78
+ layer.data.size.should == 1
79
+
80
+ # uniq check
81
+ layer.append_data([{ :x => 0, :y => 1 }])
82
+ layer.append_data([{ :x => 0, :y => 1 }])
83
+ layer.data.size.should == 1
84
+ layer.append_data([{ :x => 2, :y => 1 }])
85
+ layer.data.size.should == 2
86
+ end
87
+
88
+ should 'has ability to filter bad records' do
89
+ @tg.add_layer
90
+ layer = @tg.layers.last
91
+ layer.data.size.should == 0
92
+ layer.append_data([{ :x => 0, :y => 1 }])
93
+ layer.data.size.should == 1
94
+
95
+ # uniq check
96
+ layer.append_data([{ :z => 0, :y => 1 }])
97
+ layer.append_data([{}])
98
+ layer.data.size.should == 1
99
+ end
100
+
101
+ end
102
+
103
+
104
+ end