bio-svgenes 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,13 @@
1
+ class MiniFeature
2
+ attr_accessor :start, :end, :strand, :exons, :utrs, :block_gaps, :segment_height, :id
3
+ def initialize(args)
4
+ @start = args[:start]
5
+ @end = args[:end]
6
+ @strand = args[:strand]
7
+ @exons = args[:exons] || []
8
+ @utrs = args[:utrs] || [] #start, ennd, strand, arg[:exons], arg[:utrs]
9
+ @block_gaps = []
10
+ @id = args[:id]
11
+ @segment_height = args[:segment_height]
12
+ end
13
+ end
@@ -0,0 +1,265 @@
1
+ class Page
2
+
3
+ def initialize(args)
4
+ @height = args[:height]
5
+ @width = args[:width]
6
+ @svg = SVGEE.new(args)
7
+ @scale_start = 1.0/0.0
8
+ @scale_stop = -1.0/0.0
9
+ @tracks = [] #array of track objects with loads of features in it...
10
+ @nt_per_percent = 1;
11
+ @num_intervals = args[:number_of_intervals]
12
+ @track_top = 30
13
+
14
+ def @svg.update_height(height)
15
+ @height = height
16
+ end
17
+
18
+ def @svg.update_width(width)
19
+ @width = width
20
+ end
21
+ end
22
+
23
+ def self.from_json(args)
24
+ require 'rubygems'
25
+ require 'JSON'
26
+ data = JSON.parse(File.open(args[:json], 'r').read)
27
+ p = Page.new(:width => data["Page"]["width"],
28
+ :height => data["Page"]["height"],
29
+ :number_of_intervals => data["Page"]["intervals"])
30
+ data["Tracks"].each do |track|
31
+ #prep the track args
32
+ track_args = track.dup
33
+ track_args.delete("file")
34
+ track_args.delete("file_type")
35
+ track_args = track_args.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
36
+ ##convert any of the pre-made gradient strings in the keys to symbol
37
+ track_args.each_key do |k|
38
+ next unless track_args[k].respond_to?(:to_sym)
39
+ track_args[k] = track_args[k].to_sym if Glyph.gradients.include?(track_args[k].to_sym)
40
+ end
41
+
42
+ svg_track = p.add_track(track_args)
43
+ features = [] ##set up the features...
44
+
45
+ case track["file_type"] ##deal with the gff and data files
46
+ when "gff"
47
+ groups = {}
48
+ parentless_features = []
49
+ Page.parse_gff(track["file"]).each do |gff| #gets features in a list and links their children to them as members of the array at the key
50
+ parent_id = gff.attributes.select { |a| a.first == 'Parent'}
51
+ if parent_id.empty?
52
+ parentless_features << gff
53
+ else
54
+ if groups[parent_id.first.last].nil?
55
+ groups[parent_id.first.last] = []
56
+ groups[parent_id.first.last] << gff
57
+ else
58
+ groups[parent_id.first.last] << gff
59
+ end
60
+ end
61
+ end
62
+ #now flick through the parentless features and add any exons / UTRs
63
+ parentless_features.each do |plf|
64
+ gff_id = plf.attributes.select {|a| a.first == 'ID'}
65
+ gff_id = gff_id.first.last
66
+ exons = []
67
+ utrs = []
68
+ children = groups[gff_id] || children = []
69
+ children.each do |child|
70
+ if child.feature == 'exon' or child.feature == 'CDS'
71
+ exons += [child.start,child.end]
72
+ elsif child.feature =~ /utr/i
73
+ utrs += [child.start,child.end]
74
+ end
75
+ end
76
+ features << MiniFeature.new(:start => plf.start, :end => plf.end, :exons => exons, :utrs => utrs, :strand => plf.strand, :id => gff_id)
77
+ end #parentless features end
78
+ when "data"
79
+ ##each line is a data feature
80
+ File.open(track["file"], "r").each do |line|
81
+ start,stop,value = line.split(/\t/)
82
+ features << MiniFeature.new(:start => start.to_i, :end => stop.to_i, :segment_height => value.to_f)
83
+ end
84
+ end #data end
85
+ features.each {|f| svg_track.add(f) }
86
+ end #track end
87
+ p.write(args[:outfile])
88
+ end
89
+
90
+ def self.parse_gff(gff_file)
91
+ require 'bio'
92
+ a = []
93
+ File.open( gff_file ).each do |line|
94
+ next if line =~ /^#/
95
+ a << Bio::GFF::GFF3::Record.new(line)
96
+ end
97
+ a
98
+ end
99
+
100
+ def add_track(args)
101
+ #sort out the colour/gradient options
102
+ [:fill_color, :exon_fill_color, :utr_fill_color].each do |colour_tag|
103
+ if Glyph.gradients.include?(args[colour_tag])
104
+ @svg.gradient(Glyph.gradient(args[colour_tag]) )
105
+ args[colour_tag] = "url(##{args[colour_tag]})"
106
+ elsif
107
+ args[colour_tag].instance_of?(Hash)
108
+ @svg.gradient(args[colour_tag])
109
+ args[colour_tag] = "url(##{args[colour_tag][:id]})"
110
+ end
111
+ end
112
+ @tracks << Track.new(args)
113
+ return @tracks.last
114
+ end
115
+
116
+ def get_limits
117
+ @tracks.each do |track|
118
+ lowest = track.features.sort {|x,y| x.start <=> y.start}.first.start
119
+ highest = track.features.sort {|x,y| y.end <=> x.end}.first.end
120
+ @scale_start = lowest if lowest < @scale_start
121
+ @scale_stop = highest if highest > @scale_stop
122
+ @nt_per_px_x = (@scale_stop - @scale_start).to_f / @width.to_f
123
+ end
124
+ end
125
+
126
+ def draw_scale
127
+ Glyph.scale(:start => @scale_start,
128
+ :stop => @scale_stop,
129
+ :number_of_intervals => @num_intervals, :page_width => @width).each {|g| @svg.add_primitive(g)}
130
+ end
131
+
132
+ def draw_label(args)
133
+ Glyph.label(:text => args[:text],
134
+ :x => args[:x],
135
+ :y => args[:y] ).each {|g| @svg.add_primitive(g)}
136
+ end
137
+
138
+ def draw_generic(args) #remember presentation info comes from track@args when the track is defined
139
+ Glyph.generic(args).each {|g| @svg.add_primitive(g) }
140
+ end
141
+
142
+ def draw_directed(args)
143
+ Glyph.directed(args).each {|g| @svg.add_primitive(g) }
144
+ end
145
+
146
+ def draw_transcript(args)
147
+ Glyph.transcript(args).each {|g| @svg.add_primitive(g) }
148
+ end
149
+
150
+ def draw_histogram(args)
151
+ Glyph.generic(args).each {|g| @svg.add_primitive(g) }
152
+ end
153
+
154
+ def draw_features(track) #sort out the input information into a user friendly format..
155
+ if [:histogram, "histogram"].include?(track.glyph) #do different stuff for data tracks...
156
+
157
+ y = @track_top + (track.track_height)
158
+ max = track.max_y ? track.max_y : track.features.sort {|a,b| a.segment_height <=> b.segment_height}.last.segment_height
159
+ min = 0
160
+ if track.label
161
+ draw_label(:text => track.name, :y => @track_top += 30, :x => 3 )
162
+ end
163
+ draw_label(:text => max, :x => to_px(@scale_stop - @scale_start ) + 5, :y => @track_top + 5)
164
+ draw_label(:text => min, :x => to_px(@scale_stop - @scale_start ) + 5, :y => @track_top + track.track_height + 5)
165
+ data_interval = max - min
166
+ data_per_px = track.track_height.to_f / data_interval.to_f
167
+ track.features.each do |f|
168
+ x = to_px(f.start - @scale_start)
169
+ width = to_px( (f.end - @scale_start) - (f.start - @scale_start) )
170
+ height = f.segment_height.to_f * data_per_px
171
+ y = @track_top + track.track_height - height + 5
172
+ #$stderr.puts f.segment_height, data_per_px, data_interval, max, min, "------"
173
+ self.send("draw_#{track.glyph}", {:x => x,
174
+ :y => y,
175
+ :width => width,
176
+ :height => height }.merge!(track.args) )
177
+ end
178
+ @track_top += (track.track_height) + 20
179
+ else ##following stuff for the features
180
+ if track.label
181
+ draw_label(:text => track.name, :y => @track_top += 30, :x => 3 )
182
+ end
183
+ track.get_rows ##work out how many rows and which features belong in each row...
184
+ track.features.each_with_index do |f,index|
185
+ x = to_px(f.start - @scale_start) #bottom left of feature
186
+ all_sub_blocks = []
187
+
188
+ #convert the exon and utr start stops to px start stops and px widths
189
+ exons = []
190
+ if f.exons
191
+ f.exons.each_slice(2).each do |exon|
192
+ all_sub_blocks << exon
193
+ next if exon.nil?
194
+ exons << [to_px(exon[0] - @scale_start), to_px( (exon[1] - @scale_start) - (exon[0] - @scale_start) ) ]
195
+ end
196
+ end
197
+ f.exons = exons
198
+
199
+ utrs = []
200
+ if f.utrs
201
+ f.utrs.each_slice(2).each do |utr|
202
+ all_sub_blocks << utr
203
+ next if utr.nil?
204
+ utrs << [to_px(utr[0] - @scale_start), to_px( (utr[1] - @scale_start) - (utr[0] - @scale_start) ) ]
205
+ end
206
+ end
207
+ f.utrs = utrs
208
+
209
+ #if there are any intron like gaps.. get where they would be
210
+ if not all_sub_blocks.empty?
211
+ all_sub_blocks = all_sub_blocks.sort {|a,b| a.first <=> b.first}
212
+ all_sub_blocks.each_index do |i|
213
+ next if i + 1 == all_sub_blocks.length or all_sub_blocks[i].last >= all_sub_blocks[i + 1].first #skip if there is no gap
214
+ f.block_gaps << [to_px(all_sub_blocks[i].last - @scale_start) , to_px( (all_sub_blocks[i + 1].first - @scale_start) - (all_sub_blocks[i].last - @scale_start) ) ]
215
+ end
216
+ end
217
+
218
+ width = to_px( (f.end - @scale_start) - (f.start - @scale_start) )
219
+
220
+ y = @track_top + (track.feature_rows[index] * 2 * track.feature_height)
221
+
222
+ self.send("draw_#{track.glyph}", {:x => x,
223
+ :y => y,
224
+ :width => width,
225
+ :strand => f.strand,
226
+ :exons => f.exons,
227
+ :utrs => f.utrs,
228
+ :block_gaps => f.block_gaps,
229
+ :height => track.feature_height
230
+ }.merge!(track.args) )
231
+
232
+ if f.id
233
+ draw_label(:y => y, :x => x + width +2, :text => f.id)
234
+ end
235
+ end
236
+ @track_top += (track.feature_height * track.number_rows * 2) + 20
237
+ end
238
+
239
+ @height = @track_top + 100 #fudge...
240
+ @svg.update_height(@height)
241
+ @svg.update_width(@width + 200)
242
+
243
+ end
244
+
245
+ def to_px(num)
246
+ (num.to_f / @nt_per_px_x.to_f)
247
+ end
248
+
249
+ def get_markup
250
+ get_limits
251
+ draw_scale
252
+ @tracks.each do |track|
253
+ draw_features(track)
254
+ end
255
+ @svg.draw
256
+ end
257
+
258
+ def draw
259
+ puts get_markup
260
+ end
261
+
262
+ def write(file)
263
+ File.open(file, 'w').write(get_markup)
264
+ end
265
+ end
@@ -0,0 +1,17 @@
1
+ class Primitive
2
+ attr_reader :primitive
3
+
4
+ def initialize(primitive,args)
5
+ @primitive = primitive
6
+ args.each_key do |k|
7
+ self.instance_variable_set("@#{k}", args[k])
8
+ end
9
+ end
10
+
11
+ def update(args)
12
+ args.each_key do |k|
13
+ self.instance_variable_set("@#{k}", args[k])
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,129 @@
1
+ class SVGEE
2
+ attr_reader :primitives, :defs, :supported_primitives
3
+ def initialize(args)
4
+ opts = {:width => '100%', :height => '100%'}.merge!(args)
5
+ @width = opts[:width]
6
+ @height = opts[:height]
7
+ @style= opts[:style]
8
+ @primitives = []
9
+ @defs = []
10
+ @supported_primitives = [:circle, :rectangle, :ellipse, :line, :polyline, :text]
11
+ end
12
+
13
+ def open_tag
14
+ %{<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="#{@width}" height="#{@height}" style="#{@style}" xmlns:xlink="http://www.w3.org/1999/xlink">}
15
+ end
16
+
17
+ def close_tag
18
+ %{</svg>}
19
+ end
20
+
21
+ private
22
+ def add(primitive_string)
23
+ @primitives << primitive_string
24
+ end
25
+
26
+ def add_def(definition_string)
27
+ @defs << definition_string
28
+ end
29
+
30
+ def make_tag(primitive, args)
31
+ if args.has_key?(:link)
32
+ add link_and_tag(primitive, args)
33
+ else
34
+ add self.send("#{primitive}_tag", args)
35
+ end
36
+ return Primitive.new(primitive, args)
37
+ end
38
+
39
+ def link_and_tag(primitive, args)
40
+ %{<a xlink:href="#{args[:link][:href]}" target="#{args[:link][:target]}">} + self.send("#{primitive}_tag", args) + %{</a>}
41
+ end
42
+
43
+ def circle_tag(a = {})
44
+ %{<circle cx="#{a[:x_center]}" cy="#{a[:y_center]}" r="#{a[:radius]}" fill="#{a[:fill_color]}" } + common_attributes(a) + %q{/>}
45
+ end
46
+
47
+ def rectangle_tag(a = {})
48
+ %{<rect x="#{a[:x]}" y="#{a[:y]}" width="#{a[:width]}" height="#{a[:height]}" fill="#{a[:fill_color]}" rx="#{a[:x_round]}" ry="#{a[:y_round]}" } + common_attributes(a) + %q{/>}
49
+ end
50
+
51
+ def ellipse_tag(a = {})
52
+ %{<ellipse cx="#{a[:x_center]}" cy="#{a[:y_center]}" rx="#{a[:x_radius]}" ry="#{a[:y_radius]}" stroke="#{a[:stroke]}" stroke-width="#{a[:stroke_width]}" fill="#{a[:fill_color]}" style="#{a[:style]}" />}
53
+ end
54
+
55
+ def line_tag(a = {})
56
+ %{<line x1="#{a[:x1]}" y1="#{a[:y1]}" x2="#{a[:x2]}" y2="#{a[:y2]}" } + common_attributes(a) + %q{/>}
57
+ end
58
+
59
+ def polyline_tag(a={})
60
+ %{<polyline points="#{a[:points]}" fill="#{a[:fill]}" } + common_attributes(a) + %{ />}
61
+ end
62
+
63
+ def polygon_tag(a={})
64
+ %{<polygon points="#{a[:points]}" fill="#{a[:fill_color]}" } + common_attributes(a) + %{ />}
65
+ end
66
+
67
+ def text_tag(a = {})
68
+ %{<text x="#{a[:x]}" y="#{a[:y]}" fill="#{a[:fill]}" transform="#{a[:transform]}" style="#{a[:style]}">#{a[:text]}</text>}
69
+ end
70
+
71
+ def common_attributes(a={})
72
+ %{stroke="#{a[:stroke]}" stroke-width="#{a[:stroke_width]}" style="#{a[:style]}"}
73
+ end
74
+
75
+ def method_missing(primitive, args={}) #only used to dynamically select the primitive type..
76
+ raise NoMethodError if not self.supported_primitives.include?(primitive) #we're only doing the listed primitive types...
77
+ self.send "make_tag", primitive, args
78
+ end
79
+
80
+ public
81
+ def add_primitive(primitive_object) #adds a primitive object to the SVG
82
+ args = {}
83
+ primitive_object.instance_variables.each{|v| args[v.gsub(/@/,"").to_sym] = primitive_object.instance_variable_get(v) }
84
+ primitive_string = args.delete(:primitive)
85
+ make_tag(primitive_string, args)
86
+ end
87
+
88
+ def gradient(a)
89
+ definition_string = case a[:type]
90
+ when :radial
91
+ %{<radialGradient id="#{a[:id]}" cx="#{a[:cx]}%" cy="#{a[:cy]}%" r="#{a[:r]}%" fx="#{a[:fx]}%" fy="#{a[:fy]}%">}
92
+ else
93
+ %{<linearGradient id="#{a[:id]}" x1="#{a[:x1]}%" x2="#{a[:x2]}%" y1="#{a[:y1]}%" y2="#{a[:y2]}%">}
94
+ end
95
+ a[:stops].each do |s|
96
+ definition_string = definition_string + "\n" + %{<stop offset="#{s[:offset]}%" style="stop-color:#{s[:color]};stop-opacity:#{s[:opacity]}" />}
97
+ end
98
+ add_def definition_string + (a[:type] == :linear ? '</linearGradient>' : '</radialGradient>')
99
+ end
100
+
101
+ def draw
102
+ head = self.open_tag
103
+ defstring = ""
104
+ defstring = "<defs>\n" + self.defs.join("\n") + "\n </defs>\n" if not defs.empty?
105
+ shapes = self.primitives.join("\n")
106
+ close = self.close_tag
107
+ head + defstring + shapes + close
108
+ end
109
+
110
+ end
111
+
112
+ =begin How to use SVGEE
113
+ s = SVGEE.new
114
+ s.gradient(:radial => "grad1", :cx => 50, :cy => 50, :r => 50, :fx => 50, :fy => 50, :stops => [ {:offset => 0, :color => 'rgb(255,255,255)', :opacity => 0}, {:offset => 100, :color => 'rgb(0,0,255)', :opacity => 1},] )
115
+ s.circle(:x_center => 40, :y_center => 40, :radius => 20, :fill_color => "url(#grad1)")
116
+ s.circle(:x_center => 250, :y_center => 250, :radius => 20, :fill_color => "url(#grad1)", :link => {:href => "http://www.bbc.co.uk"})
117
+ s.rectangle(:x => 125, :y => 125, :width => 100, :height => 50, :fill_color => 'red', :stroke => 'black', :stroke_width => 2, :style => "fill-opacity:0.1;stroke-opacity:0.9")
118
+ s.rectangle(:x => 125, :y => 125, :round_x => 5, :round_y => 5, :width => 100, :height => 50, :fill_color => 'red', :stroke => 'black', :stroke_width => 2, :style => "fill-opacity:0.1;stroke-opacity:0.9")
119
+ prim = s.ellipse(:x_center => 100, :y_center => 190, :x_radius => 40, :y_radius => 20, :fill_color => 'green', :stroke => 'black')
120
+ s.line(:x1 => 10, :y1 => 10, :x2 => 145, :y2 => 145, :stroke_width => 5, :stroke => 'blue')
121
+ s.polyline(:points => "2,2 400,440 600,440", :stroke_width => 10, :stroke => "#f00", :fill => "none")
122
+
123
+ s.text(:x => 100, :y => 100, :fill => 'red', :text => "Look! A circle!", :style => "letter-spacing:2;font-family:Arial")
124
+ s.text(:x => 100, :y => 400, :fill => 'green', :text => "This one is a link", :link => {:href => "http://www.bbc.co.uk"})
125
+ prim.update(:x_center => 200)
126
+ s.add_primitive(prim) #add one of the returned, updated Primitive objects
127
+
128
+ s.draw
129
+ =end
@@ -0,0 +1,49 @@
1
+ class Track
2
+ attr_reader :glyph, :name, :label, :args, :track_height, :scale, :max_y
3
+ attr_accessor :features, :feature_rows, :name, :number_rows, :feature_height
4
+ def initialize(args)
5
+ @args = {:glyph => :generic,
6
+ :name => "feature_track",
7
+ :label => true,
8
+ :feature_height => 10,
9
+ :track_height => nil }.merge!(args)
10
+ @glyph = @args[:glyph]
11
+ @name = @args[:name]
12
+ @label = @args[:label]
13
+ @track_height = @args[:track_height]
14
+ @features = []
15
+ @feature_rows = []
16
+ @scale = @args[:scale]
17
+ @feature_height = @args[:feature_height]
18
+ @number_of_rows = 1
19
+ @max_y = args[:max_y]
20
+
21
+
22
+ end
23
+
24
+
25
+ def add(feature)
26
+ @features << feature
27
+ end
28
+
29
+ def get_rows
30
+ #works out how many rows are needed per track for overlapping features
31
+ #and which row each feature should be in
32
+ current_row = 1
33
+ @feature_rows = Array.new(@features.length,1)
34
+ @features.each_with_index do |f1, i|
35
+ @features.each_with_index do |f2, j|
36
+ next if i == j or j <= i
37
+ if overlaps(f1,f2)
38
+ @feature_rows[i] += 1
39
+ end
40
+ end
41
+ @number_rows = @feature_rows.max
42
+ end
43
+ end
44
+
45
+ def overlaps(f1, f2)
46
+ (f1.start >= f2.start and f1.start <= f2.end) or (f1.end >= f2.start and f1.end <= f2.end)
47
+ end
48
+
49
+ end