Created
March 22, 2013 14:15
-
-
Save radaniba/5221583 to your computer and use it in GitHub Desktop.
For when you have to label the tips of a phylogeny in a systematic way. Rather than "point and click" within Dendroscope, this script takes a .den/dendro file and colors the tips according to a "color description" file. This is a simple csv file with taxa labels and a corresponding color. The color may either be an RGB triplet or a scalar value …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Color the node labels in a Dendroscope tree. | |
### IMPORTS | |
require 'test/unit/assertions' | |
require 'optparse' | |
require 'pp' | |
require 'csv' | |
require 'ostruct' | |
require 'date' | |
require 'time' | |
include Test::Unit::Assertions | |
### CONSTANTS & DEFINES | |
### IMPLEMENTATION | |
def interpolate(str, sub_hash) | |
return str.gsub(/\{([^}]+)\}/) { |m| | |
sub_hash[$1] | |
} | |
end | |
### MAIN | |
class ColorMapper | |
attr_accessor(:min, :max, :range) | |
def initialize(vals) | |
@max, @min = vals.max.to_f(), vals.min.to_f() | |
@range = @max - @min | |
end | |
def map(val) | |
norm_val = normalise_val(val) | |
val255 = (norm_val * 255).round() | |
clrs = [val255, 255 - val255, val255].each { |v| | |
v.to_i.to_s | |
} | |
return clrs.join(' ') | |
end | |
def normalise_val(val) | |
# get a val from 0 to 1 | |
return (val - @min) / @range | |
end | |
end | |
# Return a mapping between label names and colors | |
# | |
def parse_clr_file(clr_file, map_to_clr) | |
clr_map = {} | |
# read file | |
CSV::Reader.parse(File.open(clr_file, 'rb')) { |row| | |
#assert_equal(2, row.length, "row length should be 2") | |
label, color = row[0].strip(), row[1].strip() | |
if map_to_clr == false | |
assert_equal(3, color.split(' ').length(), | |
"color '#{color}' looks malformed") | |
end | |
clr_map[label] = color | |
} | |
# map to colors if need be | |
if (map_to_clr) | |
clr_map.each_pair { |k,v| | |
clr_map[k] = v.to_f() | |
} | |
mapper = ColorMapper.new(clr_map.values) | |
clr_map.each_pair { |k,v| | |
clr_map[k] = mapper.map(v) | |
} | |
end | |
return clr_map | |
end | |
# Parse commandline arguments. | |
# | |
def parse_clargs(arg_arr) | |
clopts = { | |
:save => "{root}-colored.{ext}", | |
:overwrite => false, | |
:def_clr => "0 0 0", | |
:map_to_clr => false, | |
} | |
OptionParser.new { |opts| | |
opts.program_name = __FILE__ | |
opts.banner = "Color the nodes in a dendroscope file" | |
opts.separator("") | |
opts.separator("The input is a CSV file of coloring instructions and one") | |
opts.separator("or more Dendroscope tree files. The coloring instructions") | |
opts.separator("consist of one row for each node to be labelled,") | |
opts.separator("consisting of:") | |
opts.separator("") | |
opts.separator(" <label>, <fgcolor>") | |
opts.separator("") | |
opts.separator("where the color is a RGB 256 triplet (e.g. '0 128 255').") | |
opts.separator("") | |
opts.separator("Usage: #{opts.program_name} [options] CLRFILE TREEFILE1 ...]") | |
opts.on('-h', '--help', 'Display this screen') { | |
puts opts | |
exit | |
} | |
opts.on('-m', '--default-color STR', | |
"The default color nodes will be given") { |v| | |
clopts[:def_clr] = v | |
} | |
opts.on('-m', '--map-to-colors', | |
"The coloring instructions give a float value which will be mapped to a color") { | |
clopts[:map_to_clr] = true | |
} | |
opts.on('', '--save STR', | |
"Name output files according this template") { |v| | |
clopts[:save] = v | |
} | |
opts.on('-o', '--overwrite', | |
"Overwrite pre-existing files") { | |
clopts[:overwrite] = true | |
} | |
begin | |
opts.parse!(arg_arr) | |
rescue OptionParser::InvalidOption => e | |
puts e | |
puts opts | |
exit 1 | |
end | |
} | |
pargs = arg_arr | |
assert(2 <= pargs.length, "need files to work on") | |
return clopts, pargs[0], pargs[1, pargs.length] | |
end | |
# Main script functionality. | |
# | |
def main() | |
clopts, clr_file, tree_files = parse_clargs(ARGV) | |
clr_table = parse_clr_file(clr_file, clopts[:map_to_clr]) | |
tree_files.each { |f| | |
# slurp ... | |
puts "== Reading '#{f}' ..." | |
converted_lines = [] | |
tree_data = File.open(f, 'rb').read() | |
# give each node a color, whether it wants it or not | |
tree_data.gsub!(Regexp.new('^(\d+: .*) lb=(.+)$')) { |m| | |
match_str = m.to_s() | |
if (match_str.include?(" lc=")) | |
# color already defined | |
match_str | |
else | |
"#{$1} lc=#{clopts[:def_clr]} lb=#{$2}" | |
end | |
} | |
# add color labels | |
clr_table.each_pair { |label, color| | |
pp color | |
tree_data.gsub!( | |
Regexp.new("lc=(.*) (lb='#{Regexp.escape(label)}')"), | |
"lc=#{color} " + '\2' | |
) | |
} | |
# write output | |
# make filename | |
ext = File.extname(f) | |
subs = { | |
"ext" => ext[1, ext.length], | |
"base" => File.basename(f), | |
"root" => File.basename(f, ext), | |
"date" => Date.today.to_s(), | |
"time" => Time.now.strftime(fmt='%T'), | |
"datetime" => DateTime.now.strftime(fmt='%F T%T'), | |
} | |
out_name = interpolate(clopts[:save], subs) | |
puts "Saving drawing to '#{out_name}' ..." | |
# do the writing | |
if File.exists?(out_name) | |
assert(clopts[:overwrite], "Can't overwrite existing file '#{out_name}'") | |
end | |
File.open(out_name, 'w').write(tree_data) | |
#drwr.save(out_name) | |
puts "Saved." | |
} | |
puts "== Finished." | |
end | |
if $0 == __FILE__ | |
main() | |
end | |
### END |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment