technical_graph 0.0.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.
@@ -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