diff --git a/.gitignore b/.gitignore index c47142fb..d6fa962d 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ distribute-*.egg dammit.egg-info/ *.so *~ +.tags* diff --git a/MANIFEST.in b/MANIFEST.in index a9cbb482..50d8b3b2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,9 @@ include setup.cfg include LICENSE include distribute_setup.py include dammit/VERSION -include bin/dammit +include bin/dammit* recursive-include dammit .*.json +recursive-include dammit/viewer/templates *.html +recursive-include dammit/viewer/templates *.js +recursive-include dammit/viewer/static *.css +recursive-include dammit/viewer/static *.js diff --git a/bin/dammit-view b/bin/dammit-view new file mode 100644 index 00000000..3aaa7d62 --- /dev/null +++ b/bin/dammit-view @@ -0,0 +1,28 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +from flask import Flask, g +from dammit import common +from dammit.viewer import transcript_pane +from dammit.viewer.database import db +from dammit.viewer import static_folder, template_folder +import os + +DIRECTORY = None +ZODB_STORAGE = None + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--directory') + args = parser.parse_args() + + DIRECTORY = os.path.abspath(args.directory) + ZODB_STORAGE = 'file://' + os.path.join(DIRECTORY, common.CONFIG['settings']['database_filename']) + + app = Flask(__name__, static_folder=static_folder, template_folder=template_folder) + app.config.from_object(__name__) + app.register_blueprint(transcript_pane.views) + db.init_app(app) + + app.run(host='0.0.0.0', port=5001, debug=True) diff --git a/dammit/.config.json b/dammit/.config.json index 07f4c813..bf7dfe5c 100644 --- a/dammit/.config.json +++ b/dammit/.config.json @@ -9,7 +9,9 @@ "dammit_dir": ".dammit", "dep_dir": "dependencies", "db_dir": "databases", - + "summary_filename": "dammit.summary.json", + "database_filename": "dammit.database.fs", + "blast": { "evalue": 0.000001, "params": "" diff --git a/dammit/.databases.json b/dammit/.databases.json index 564da0fb..84e82f7f 100644 --- a/dammit/.databases.json +++ b/dammit/.databases.json @@ -43,6 +43,13 @@ "url": "ftp://cegg.unige.ch/OrthoDB8/Eukaryotes/Genes_to_OGs/ODB8_EukOGs_genes_ALL_levels.txt.gz" }, + "ncbi.tax": { + "access": "download", + "db_type": "json", + "filename": "ncbi_taxonomy.json", + "url": "https://s3-us-west-1.amazonaws.com/json-taxonomies/ncbi_taxonomy.json" + }, + "busco": { "metazoa": { diff --git a/dammit/__init__.py b/dammit/__init__.py index a785c72e..91af3eed 100644 --- a/dammit/__init__.py +++ b/dammit/__init__.py @@ -7,16 +7,18 @@ from .hits import BestHits from .crbl import CRBL -import parsers -import gff -import blast -import tasks +import fileio -import annotate -import databases -import dependencies -import common -import report +from . import parsers +from . import gff +from . import blast +from . import tasks + +from . import annotate +from . import databases +from . import dependencies +from . import common +from . import report import os rel_path = os.path.dirname(__file__) diff --git a/dammit/annotate.py b/dammit/annotate.py index 54eff343..a32a905d 100644 --- a/dammit/annotate.py +++ b/dammit/annotate.py @@ -6,11 +6,13 @@ from platform import system import sys +from doit.task import Task + from . import common from .log import LogReporter #from .crbl import CRBL from .report import get_report_tasks -from .tasks import get_transcriptome_stats_task, \ +from .tasks import get_summary_task, \ get_busco_task, \ get_group_task, \ get_link_file_task, \ @@ -24,6 +26,7 @@ get_sanitize_fasta_task, \ get_rename_transcriptome_task, \ get_transeq_task, \ + get_create_zodb_task, \ print_tasks logger = logging.getLogger(__name__) @@ -61,16 +64,14 @@ def _init_filenames(self): else: out_dir = self.args.output_dir self.directory = os.path.abspath(out_dir) - - self.stats_fn = self.transcriptome_fn + '.stats.json' self.busco_basename = '{0}.{1}.busco.results'.format(self.transcriptome_fn, self.args.busco_group) self.busco_dir = 'run_{0}'.format(self.busco_basename) - busco_summary_fn = 'short_summary_{0}'.format(self.transcriptome_fn) + busco_summary_fn = 'short_summary_{0}'.format(self.busco_basename) self.busco_summary_fn = os.path.join(self.busco_dir, busco_summary_fn) - + self.translated_fn = '{0}.pep'.format(self.transcriptome_fn) self.transdecoder_dir = '{0}.transdecoder_dir'.format(self.transcriptome_fn) @@ -81,13 +82,20 @@ def _init_filenames(self): self.transdecoder_pfam_fn = '{0}.pfam.tbl'.format(self.transdecoder_orf_fn) self.transdecoder_pep_fn = '{0}.transdecoder.pep'.format(self.transcriptome_fn) self.transdecoder_gff3_fn = '{0}.transdecoder.gff3'.format(self.transcriptome_fn) - + self.pfam_fn = '{0}.pfam.csv'.format(self.transcriptome_fn) self.rfam_fn = '{0}.rfam.tbl'.format(self.transcriptome_fn) self.orthodb_fn = '{0}.x.orthodb.maf'.format(self.transcriptome_fn) self.uniref_fn = '{0}.x.uniref.maf'.format(self.transcriptome_fn) + self.final_gff3_fn = '{0}.dammit.gff3'.format(self.transcriptome_fn) + self.final_fasta_fn = '{0}.dammit.fasta'.format(self.transcriptome_fn) + self.final_transcript_fn = '{0}.dammit.json'.format(self.transcriptome_fn) + self.transcript_info_fn = '{0}.dammit.info.csv'.format(self.transcriptome_fn) + self.summary_fn = common.CONFIG['settings']['summary_filename'] + self.database_fn = common.CONFIG['settings']['database_filename'] + self.user_pep_fn_dict = {} @@ -117,8 +125,8 @@ def run_tasks(self, doit_args=['run']): os.makedirs(self.directory) os.chdir(self.directory) - common.run_tasks(self.tasks, - doit_args, + common.run_tasks(self.tasks, + doit_args, config=self.doit_config) finally: self.logger.debug('chdir: {0}'.format(cwd)) @@ -130,27 +138,17 @@ def rename_task(self): self.names_fn, self.args.name) - def stats_task(self): - '''Calculate assembly information. First it runs some basic stats like N50 and - number of contigs, and uses the HyperLogLog counter from khmer to - estimate unique k-mers for checking redundancy. Then it runs BUSCO to - assess completeness. These tasks are grouped under the 'assess' task. - ''' - - return get_transcriptome_stats_task(self.transcriptome_fn, - self.stats_fn) - def busco_task(self): '''BUSCO assesses completeness using a series of curated databases of core conserved genes. ''' busco_cfg = common.CONFIG['settings']['busco'] - return get_busco_task(self.transcriptome_fn, - self.busco_basename, + return get_busco_task(self.transcriptome_fn, + self.busco_basename, self.database_dict['BUSCO'], - 'trans', - self.args.n_threads, + 'trans', + self.args.n_threads, busco_cfg) def transeq_task(self): @@ -170,14 +168,14 @@ def transdecoder_tasks(self): orf_cfg = common.CONFIG['settings']['transdecoder']['longorfs'] - yield get_transdecoder_orf_task(self.transcriptome_fn, + yield get_transdecoder_orf_task(self.transcriptome_fn, orf_cfg) - yield get_hmmscan_task(self.transdecoder_orf_fn, + yield get_hmmscan_task(self.transdecoder_orf_fn, self.transdecoder_pfam_fn, - self.database_dict['PFAM'], + self.database_dict['PFAM'], self.args.evalue, - self.args.n_threads, + self.args.n_threads, common.CONFIG['settings']['hmmer']['hmmscan']) yield get_remap_hmmer_task(self.transdecoder_pfam_fn, @@ -185,7 +183,7 @@ def transdecoder_tasks(self): self.pfam_fn) predict_cfg = common.CONFIG['settings']['transdecoder']['predict'] - yield get_transdecoder_predict_task(self.transcriptome_fn, + yield get_transdecoder_predict_task(self.transcriptome_fn, self.transdecoder_pfam_fn, predict_cfg) @@ -196,26 +194,26 @@ def cmscan_task(self): ''' cmscan_cfg = common.CONFIG['settings']['infernal']['cmscan'] - return get_cmscan_task(self.transcriptome_fn, + return get_cmscan_task(self.transcriptome_fn, self.rfam_fn, - self.database_dict['RFAM'], + self.database_dict['RFAM'], self.args.evalue, - self.args.n_threads, + self.args.n_threads, cmscan_cfg) def orthodb_task(self): '''Run LAST to get homologies with OrthoDB. We use LAST here because it is much faster than BLAST+, and OrthoDB is pretty huge. ''' - + lastal_cfg = common.CONFIG['settings']['last']['lastal'] orthodb = self.database_dict['ORTHODB'] - return get_lastal_task(self.transcriptome_fn, - orthodb, - self.orthodb_fn, + return get_lastal_task(self.transcriptome_fn, + orthodb, + self.orthodb_fn, True, self.args.evalue, - self.args.n_threads, + self.args.n_threads, lastal_cfg) def uniref_task(self): @@ -243,17 +241,37 @@ def user_crb_tasks(self): fn = '{0}.x.{1}.crbb.tsv'.format(self.transcriptome_fn, key) self.user_pep_fn_dict[key] = fn - yield get_crb_blast_task(self.transcriptome_fn, - key, - fn, + yield get_crb_blast_task(self.transcriptome_fn, + key, + fn, self.args.evalue, - crb_blast_cfg, + crb_blast_cfg, self.args.n_threads) + def summary_task(self): + '''Calculate assembly information. First it runs some basic stats like N50 and + number of contigs, and uses the HyperLogLog counter from khmer to + estimate unique k-mers for checking redundancy. Then it runs BUSCO to + assess completeness. These tasks are grouped under the 'assess' task. + ''' + + return get_summary_task(self.final_fasta_fn, + self.names_fn, + self.directory, + self.final_gff3_fn, + self.busco_summary_fn, + self.summary_fn, + self.transcript_info_fn) + + def create_zodb_task(self): + return get_create_zodb_task(self.final_gff3_fn, + self.transcript_info_fn, + self.database_fn) + def get_tasks(self): yield self.rename_task() - yield self.stats_task() + #yield self.stats_task() yield self.busco_task() #yield self.transeq_task() for task in self.transdecoder_tasks(): @@ -265,10 +283,11 @@ def get_tasks(self): for task in self.user_crb_tasks(): yield task - self.outputs, report_tasks = get_report_tasks(self.transcriptome_fn, - self, - self.database_dict, - n_threads=self.args.n_threads) + report_tasks = get_report_tasks(self.transcriptome_fn, + self, + self.database_dict, + n_threads=self.args.n_threads) for task in report_tasks: yield task - + yield self.summary_task() + yield self.create_zodb_task() diff --git a/dammit/app.py b/dammit/app.py index b5cd0794..e416c265 100644 --- a/dammit/app.py +++ b/dammit/app.py @@ -22,7 +22,7 @@ def __init__(self, arg_src=sys.argv[1:]): self.meta = '{0}\n{1} {2}'.format(common.CONFIG['meta']['description'], ', '.join(common.CONFIG['meta']['authors']), common.CONFIG['meta']['date']) - + self.parser = self.get_parser() self.args = self.parser.parse_args(arg_src) @@ -40,7 +40,7 @@ def get_parser(self): ) parser.add_argument('--debug', action='store_true', default=False) - parser.add_argument('--version', action='version', + parser.add_argument('--version', action='version', version='%(prog)s ' +__version__) subparsers = parser.add_subparsers(title='dammit subcommands') @@ -53,16 +53,16 @@ def add_common_args(parser): Args: parser (object): The parser to which arguments will be added. ''' - parser.add_argument('--database-dir', - default=None, + parser.add_argument('--database-dir', + default=None, help='Directory to store databases. Existing'\ ' databases will not be overwritten.'\ ' By default, the database directory is'\ ' $HOME/.dammit/databases.' ) - parser.add_argument('--full', - action='store_true', + parser.add_argument('--full', + action='store_true', default=False, help='Do complete run with uniref90. By default'\ ' uniref90 is left out, as it is huge '\ @@ -70,7 +70,7 @@ def add_common_args(parser): ' time.' ) - parser.add_argument('--busco-group', + parser.add_argument('--busco-group', default='metazoa', choices=['metazoa', 'eukaryota', 'vertebrata', 'arthropoda'], @@ -79,6 +79,7 @@ def add_common_args(parser): ) parser.add_argument('--verbosity', default=0, + type=int, choices=[0,1,2], help='Verbosity level for doit tasks.' ) @@ -86,7 +87,7 @@ def add_common_args(parser): ''' Add the databases subcommand. ''' - desc = '''Check for databases and optionally download and prepare them + desc = '''Check for databases and optionally download and prepare them for use. By default, only check their status.''' databases_parser = subparsers.add_parser( 'databases', @@ -94,7 +95,7 @@ def add_common_args(parser): help=desc ) - databases_parser.add_argument('--install', + databases_parser.add_argument('--install', action='store_true', default=False, help='Install missing databases. Downloads' @@ -107,8 +108,8 @@ def add_common_args(parser): ''' Add the dependencies subcommand. ''' - desc = '''Checks for dependencies on system PATH. Unlike with the - databases, dependencies are not downloaded when missing and must + desc = '''Checks for dependencies on system PATH. Unlike with the + databases, dependencies are not downloaded when missing and must be installed by the user.''' dependencies_parser = subparsers.add_parser( 'dependencies', @@ -120,10 +121,10 @@ def add_common_args(parser): ''' Add the annotation subcommand. ''' - desc = '''The main annotation pipeline. Calculates assembly stats; - runs BUSCO; runs LAST against OrthoDB (and optionally uniref90), - HMMER against Pfam, Inferal against Rfam, and Conditional Reciprocal - Best-hit Blast against user databases; and aggregates all results in + desc = '''The main annotation pipeline. Calculates assembly stats; + runs BUSCO; runs LAST against OrthoDB (and optionally uniref90), + HMMER against Pfam, Inferal against Rfam, and Conditional Reciprocal + Best-hit Blast against user databases; and aggregates all results in a properly formatted GFF3 file.''' annotate_parser = subparsers.add_parser( 'annotate', @@ -132,7 +133,7 @@ def add_common_args(parser): help=desc ) - annotate_parser.add_argument('transcriptome', + annotate_parser.add_argument('transcriptome', help='FASTA file with the transcripts to be'\ ' annotated.' ) @@ -154,22 +155,22 @@ def add_common_args(parser): ' searches.' ) - annotate_parser.add_argument('-o', '--output-dir', + annotate_parser.add_argument('-o', '--output-dir', default=None, help='Output directory. By default this will'\ ' be the name of the transcriptome file'\ ' with `.dammit` appended' ) - annotate_parser.add_argument('--n_threads', - type=int, + annotate_parser.add_argument('--n_threads', + type=int, default=1, help='Number of threads to pass to programs'\ ' supporting multithreading' ) - annotate_parser.add_argument('--user-databases', - nargs='+', + annotate_parser.add_argument('--user-databases', + nargs='+', default=[], help='Optional additional protein databases. '\ ' These will be searched with CRB-blast.' @@ -187,7 +188,7 @@ def handle_databases(self): databases.DatabaseHandler(self.args).handle() - + def handle_dependencies(self): common.print_header('submodule: dependencies', level=1) @@ -203,5 +204,3 @@ def handle_annotate(self): db_handler.check_or_fail() annotate.AnnotateHandler(self.args, db_handler.databases).handle() - - diff --git a/dammit/databases.py b/dammit/databases.py index cbb42dbd..bf1db841 100644 --- a/dammit/databases.py +++ b/dammit/databases.py @@ -10,6 +10,7 @@ from . import common from .log import LogReporter from .tasks import get_download_and_gunzip_task, \ + get_download_task, \ get_hmmpress_task, \ get_cmpress_task, \ get_download_and_untar_task, \ @@ -31,7 +32,7 @@ def __init__(self, args): except KeyError: self.logger.debug('no DAMMIT_DB_DIR or --database-dir, using'\ ' default') - directory = os.path.join(common.get_dammit_dir(), + directory = os.path.join(common.get_dammit_dir(), common.CONFIG['settings']['db_dir']) else: directory = args.database_dir @@ -59,7 +60,7 @@ def handle(self, doit_args=['run']): missing = self.check() print_tasks(self.tasks, logger=self.logger) - + if self.args.install: if missing: common.print_header('Installing databases', level=2) @@ -104,8 +105,8 @@ def check(self): return missing def get_tasks(self): - '''Generate tasks for installing the bundled databases. - + '''Generate tasks for installing the bundled databases. + These tasks download the databases, unpack them, and format them for use. Current bundled databases are: @@ -113,7 +114,7 @@ def get_tasks(self): * Rfam (RNA models) * OrthoDB8 (conserved ortholog groups) * uniref90 (protiens, if --full selected) - + User-supplied databases are downloaded separately. Returns: @@ -170,20 +171,28 @@ def get_tasks(self): BUSCO = os.path.join(self.directory, 'buscodb') tasks.append( # That top-level path is given to the download task: - get_download_and_untar_task(common.DATABASES['busco'][self.args.busco_group]['url'], + get_download_and_untar_task(common.DATABASES['busco'][self.args.busco_group]['url'], BUSCO, label=self.args.busco_group) ) # The untarred arhive has a folder named after the group: databases['BUSCO'] = os.path.abspath(os.path.join(BUSCO, self.args.busco_group)) + NCBI_TAXA = os.path.join(self.directory, + common.DATABASES['ncbi.tax']['filename']) + tasks.append( + get_download_task(common.DATABASES['ncbi.tax']['url'], + NCBI_TAXA) + ) + databases['NCBI_TAXA'] = os.path.abspath(NCBI_TAXA) + # Get uniref90 if the user specifies # Ignoring this until we have working CRBL if self.args.full: - UNIREF = os.path.join(self.directory, + UNIREF = os.path.join(self.directory, common.DATABASES['uniref90']['filename']) tasks.append( - get_download_and_gunzip_task(common.DATABASES['uniref90']['url'], + get_download_and_gunzip_task(common.DATABASES['uniref90']['url'], UNIREF) ) tasks.append( @@ -193,4 +202,3 @@ def get_tasks(self): databases['UNIREF'] = os.path.abspath(UNIREF) return databases, tasks - diff --git a/dammit/fileio/__init__.py b/dammit/fileio/__init__.py new file mode 100644 index 00000000..1e9e9f8c --- /dev/null +++ b/dammit/fileio/__init__.py @@ -0,0 +1 @@ +from . import maf diff --git a/dammit/fileio/base.py b/dammit/fileio/base.py new file mode 100644 index 00000000..9acc04b7 --- /dev/null +++ b/dammit/fileio/base.py @@ -0,0 +1,14 @@ +class BaseParser(object): + + def __init__(self, filename): + self.filename = filename + +class ChunkParser(BaseParser): + + def __init__(self, filename, chunksize=10000): + self.chunksize = chunksize + super(ChunkParser, self).__init__(filename) + + def __iter__(self): + raise NotImplementedError() + yield diff --git a/dammit/fileio/maf.py b/dammit/fileio/maf.py new file mode 100644 index 00000000..65a0c1a3 --- /dev/null +++ b/dammit/fileio/maf.py @@ -0,0 +1,90 @@ +import pandas as pd +import numpy as np +from .base import ChunkParser + +class MafParser(ChunkParser): + + def __init__(self, *args, **kwargs): + self.LAMBDA = None + self.K = None + super(MafParser, self).__init__(*args, **kwargs) + + def __iter__(self): + '''Iterator yielding DataFrames of length chunksize holding MAF alignments. + + An extra column is added for bitscore, using the equation described here: + http://last.cbrc.jp/doc/last-evalues.html + + Args: + fn (str): Path to the MAF alignment file. + chunksize (int): Alignments to parse per iteration. + Yields: + DataFrame: Pandas DataFrame with the alignments. + ''' + data = [] + with open(self.filename) as fp: + while (True): + try: + line = fp.next().strip() + except StopIteration: + break + if not line: + continue + if line.startswith('#'): + if 'lambda' in line: + meta = line.strip(' #').split() + meta = {k:v for k, _, v in map(lambda x: x.partition('='), meta)} + self.LAMBDA = float(meta['lambda']) + self.K = float(meta['K']) + else: + continue + if line.startswith('a'): + cur_aln = {} + + # Alignment info + tokens = line.split() + for token in tokens[1:]: + key, _, val = token.strip().partition('=') + cur_aln[key] = float(val) + + # First sequence info + line = fp.next() + tokens = line.split() + cur_aln['s_name'] = tokens[1] + cur_aln['s_start'] = int(tokens[2]) + cur_aln['s_aln_len'] = int(tokens[3]) + cur_aln['s_strand'] = tokens[4] + cur_aln['s_len'] = int(tokens[5]) + + # First sequence info + line = fp.next() + tokens = line.split() + cur_aln['q_name'] = tokens[1] + cur_aln['q_start'] = int(tokens[2]) + cur_aln['q_aln_len'] = int(tokens[3]) + cur_aln['q_strand'] = tokens[4] + cur_aln['q_len'] = int(tokens[5]) + + data.append(cur_aln) + if len(data) >= self.chunksize: + if self.LAMBDA is None: + raise Exception("old version of lastal; please update") + yield self._build_df(data) + data = [] + + if data: + yield self._build_df(data) + + def _build_df(self, data): + + def _fix_sname(name): + new, _, _ = name.partition(',') + return new + + df = pd.DataFrame(data) + df['s_name'] = df['s_name'].apply(_fix_sname) + setattr(df, 'LAMBDA', self.LAMBDA) + setattr(df, 'K', self.K) + df['bitscore'] = (self.LAMBDA * df['score'] - np.log(self.K)) / np.log(2) + + return df diff --git a/dammit/gff.py b/dammit/gff.py index 91c70949..6bc8f6f4 100644 --- a/dammit/gff.py +++ b/dammit/gff.py @@ -28,11 +28,12 @@ def write_gff3_df(df, fp): index=False, header=False, quoting=csv.QUOTE_NONE) -def maf_to_gff3_df(maf_df, tag, database=''): +def maf_to_gff3_df(maf_df, tag='', database=''): gff3_df = pd.DataFrame() gff3_df['seqid'] = maf_df['q_name'] - gff3_df['source'] = ['LAST'] * len(maf_df) + source = '{0}.LAST'.format(tag) if tag else 'LAST' + gff3_df['source'] = [source] * len(maf_df) gff3_df['type'] = ['translated_nucleotide_match'] * len(maf_df) gff3_df['start'] = maf_df['q_start'] + 1 gff3_df['end'] = maf_df['q_start'] + maf_df['q_aln_len'] + 1 @@ -43,7 +44,7 @@ def maf_to_gff3_df(maf_df, tag, database=''): def build_attr(row): data = [] data.append('ID=homology:{0}'.format(next(ID_GEN))) - data.append('Name={0}:{1}'.format(row.s_name, tag)) + data.append('Name={0}'.format(row.s_name)) data.append('Target={0} {1} {2} {3}'.format(row.s_name, row.s_start, row.s_start + row.s_aln_len, row.s_strand)) @@ -53,49 +54,16 @@ def build_attr(row): return ';'.join(data) gff3_df['attributes'] = maf_df.apply(build_attr, axis=1) - - return gff3_df - - -def blast_to_gff3_df(blast_df, prog, RBH=False, database=''): - - assert prog in ['BLASTX', 'TBLASTN', 'BLASTP'] - - gff3_df = pd.DataFrame() - gff3_df['seqid'] = blast_df['qseqid'] - gff3_df['source'] = [prog] * len(blast_df) - ftype = 'protein_match' - if prog in ['BLASTX', 'TBLASTN']: - ftype = 'translated_nucleotide_match' - gff3_df['type'] = [ftype] * len(blast_df) - - gff3_df['start'] = blast_df['qstart'] + 1 - gff3_df['end'] = blast_df['qend'] - gff3_df['score'] = blast_df['evalue'] - gff3_df['strand'] = blast_df['qstrand'] - gff3_df['phase'] = ['.'] * len(blast_df) - - def build_attr(row): - data = [] - data.append('ID=homology:{0}'.format(next(ID_GEN))) - data.append('Name={0}:{1}'.format(row.sseqid, tag)) - data.append('Target={0} {1} {2} {3}'.format(row.sseqid, row.sstart, - row.send, row.sstrand)) - if database: - data.append('database={0}'.format(database)) - - return ';'.join(data) - - gff3_df['attributes'] = blast_df.apply(build_attr, axis=1) return gff3_df -def crb_to_gff3_df(crb_df, tag, database=''): +def crb_to_gff3_df(crb_df, tag='', database=''): gff3_df = pd.DataFrame() gff3_df['seqid'] = crb_df['query'] - gff3_df['source'] = ['crb-blast'] * len(crb_df) + source = '{0}.crb-blast'.format(tag) if tag else 'crb-blast' + gff3_df['source'] = [source] * len(crb_df) gff3_df['type'] = ['translated_nucleotide_match'] * len(crb_df) gff3_df['start'] = crb_df['qstart'] + 1 gff3_df['end'] = crb_df['qend'] @@ -106,7 +74,7 @@ def crb_to_gff3_df(crb_df, tag, database=''): def build_attr(row): data = [] data.append('ID=homology:{0}'.format(next(ID_GEN))) - data.append('Name={0}:{1}'.format(row.subject, tag)) + data.append('Name={0}'.format(row.subject)) data.append('Target={0} {1} {2} {3}'.format(row.subject, row.sstart, row.send, row.sstrand)) @@ -119,11 +87,12 @@ def build_attr(row): return gff3_df -def hmmscan_to_gff3_df(hmmscan_df, tag, database=''): - +def hmmscan_to_gff3_df(hmmscan_df, tag='', database=''): + gff3_df = pd.DataFrame() gff3_df['seqid'] = hmmscan_df['query_name'] - gff3_df['source'] = ['HMMER'] * len(hmmscan_df) + source = '{0}.HMMER'.format(tag) if tag else 'HMMER' + gff3_df['source'] = [source] * len(hmmscan_df) gff3_df['type'] = ['protein_hmm_match'] * len(hmmscan_df) gff3_df['start'] = hmmscan_df['env_coord_from'] @@ -133,11 +102,11 @@ def hmmscan_to_gff3_df(hmmscan_df, tag, database=''): gff3_df['score'] = hmmscan_df['domain_i_evalue'] gff3_df['strand'] = ['.'] * len(hmmscan_df) gff3_df['phase'] = ['.'] * len(hmmscan_df) - + def build_attr(row): data = [] data.append('ID=homology:{0}'.format(next(ID_GEN))) - data.append('Name={0}:{1}'.format(row.target_name, tag)) + data.append('Name={0}'.format(row.target_name)) data.append('Target={0} {1} {2} +'.format(row.target_name, row.hmm_coord_from, row.hmm_coord_to)) @@ -153,11 +122,12 @@ def build_attr(row): return gff3_df -def cmscan_to_gff3_df(cmscan_df, tag, database=''): - +def cmscan_to_gff3_df(cmscan_df, tag='', database=''): + gff3_df = pd.DataFrame() gff3_df['seqid'] = cmscan_df['query_name'] - gff3_df['source'] = ['Infernal'] * len(cmscan_df) + source = '{0}.Infernal'.format(tag) if tag else 'Infernal' + gff3_df['source'] = [source] * len(cmscan_df) # For now, using: # http://www.sequenceontology.org/browser/current_svn/term/SO:0000122 @@ -174,7 +144,7 @@ def cmscan_to_gff3_df(cmscan_df, tag, database=''): def build_attr(row): data = [] data.append('ID=homology:{0}'.format(next(ID_GEN))) - data.append('Name={0}:{1}'.format(row.target_name, tag)) + data.append('Name={0}'.format(row.target_name)) data.append('Target={0} {1} {2} +'.format(row.target_name, row.mdl_from, row.mdl_to)) @@ -190,4 +160,3 @@ def build_attr(row): gff3_df['attributes'] = cmscan_df.apply(build_attr, axis=1) return gff3_df - diff --git a/dammit/parsers.py b/dammit/parsers.py index cc0f75b7..bdfacbe3 100644 --- a/dammit/parsers.py +++ b/dammit/parsers.py @@ -6,41 +6,41 @@ from blast import remap_blast_coords_df as remap_blast -blast_cols = [('qseqid', str), - ('sseqid', str), - ('pident', float), - ('length', int), - ('mismatch', int), +blast_cols = [('qseqid', str), + ('sseqid', str), + ('pident', float), + ('length', int), + ('mismatch', int), ('gapopen', int), - ('qstart', int), - ('qend', int), - ('sstart', int), - ('send', int), - ('evalue', float), + ('qstart', int), + ('qend', int), + ('sstart', int), + ('send', int), + ('evalue', float), ('bitscore', float)] -hmmscan_cols = [('target_name', str), - ('target_accession', str), +hmmscan_cols = [('target_name', str), + ('target_accession', str), ('tlen', int), - ('query_name', str), - ('query_accession', str), + ('query_name', str), + ('query_accession', str), ('query_len', int), - ('full_evalue', float), - ('full_score', float), - ('full_bias', float), - ('domain_num', int), - ('domain_total', int), - ('domain_c_evalue', float), + ('full_evalue', float), + ('full_score', float), + ('full_bias', float), + ('domain_num', int), + ('domain_total', int), + ('domain_c_evalue', float), ('domain_i_evalue', float), - ('domain_score', float), - ('domain_bias', float), - ('hmm_coord_from', int), - ('hmm_coord_to', int), - ('ali_coord_from', int), - ('ali_coord_to', int), - ('env_coord_from', int), - ('env_coord_to', int), - ('accuracy', float), + ('domain_score', float), + ('domain_bias', float), + ('hmm_coord_from', int), + ('hmm_coord_to', int), + ('ali_coord_from', int), + ('ali_coord_to', int), + ('env_coord_from', int), + ('env_coord_to', int), + ('accuracy', float), ('description', str)] cmscan_cols = [('target_name', str), @@ -52,8 +52,8 @@ ('mdl_to', int), ('seq_from', int), ('seq_to', int), - ('strand', str), - ('trunc', str), + ('strand', str), + ('trunc', str), ('pass', str), ('gc', float), ('bias', float), @@ -62,10 +62,10 @@ ('inc', str), ('description', str)] -gff3_transdecoder_cols = [('seqid', str), - ('feature_type', str), - ('start', int), - ('end', int), +gff3_transdecoder_cols = [('seqid', str), + ('feature_type', str), + ('start', int), + ('end', int), ('strand', str)] gff_cols = [('seqid', str), @@ -214,7 +214,7 @@ def attr_col_func(col): # Read everything into a DataFrame - for group in pd.read_table(fn, delimiter='\t', comment='#', + for group in pd.read_table(fn, delimiter='\t', comment='#', names=[k for k,_ in gff_cols], na_values='.', converters={'attributes': attr_col_func}, chunksize=chunksize, header=None, @@ -225,7 +225,7 @@ def attr_col_func(col): pd.DataFrame(list(group.attributes)), left_index=True, right_index=True) del gtf_df['attributes'] - + # Switch from [start, end] to [start, end) gtf_df.end = gtf_df.end + 1 #convert_dtypes(gtf_df, dict(gff_cols)) @@ -264,37 +264,6 @@ def crb_to_df_iter(fn, chunksize=10000, remap=False): yield group -def parse_busco(fn): - '''Parse a single BUSCO summary file to a dictionary. - - Args: - fn (str): The results file. - Returns: - dict: The parsed results. - ''' - - res = {} - with open(fn) as fp: - for ln in fp: - if ln.strip().startswith('C:'): - tokens = ln.split(',') - for token in tokens: - key, _, val = token.partition(':') - key = key.strip() - val = val.strip().strip('%') - if key == 'C': - valc, _, vald = val.partition('%') - valc = valc.strip() - vald = vald.strip('D:][%') - res['C(%)'] = valc - res['D(%)'] = vald - else: - if key != 'n': - key += '(%)' - res[key] = val.strip().strip('%') - return res - - def busco_to_df(fn_list, dbs=['metazoa', 'vertebrata']): ''' Given a list of BUSCO results from different databases, produce an appropriately multi-indexed DataFrame of the results. @@ -407,7 +376,7 @@ def build_df(data): df = pd.DataFrame(data, columns=[k for k, _ in gff3_transdecoder_cols]) convert_dtype(df, dict(gff3_transdecoder_cols)) return df - + data = [] with open(fn) as fp: for ln in fp: @@ -426,86 +395,3 @@ def build_df(data): if data: yield build_df(data) - - -def maf_to_df_iter(fn, chunksize=10000): - '''Iterator yielding DataFrames of length chunksize holding MAF alignments. - - An extra column is added for bitscore, using the equation described here: - http://last.cbrc.jp/doc/last-evalues.html - - Args: - fn (str): Path to the MAF alignment file. - chunksize (int): Alignments to parse per iteration. - Yields: - DataFrame: Pandas DataFrame with the alignments. - ''' - - def fix_sname(name): - new, _, _ = name.partition(',') - return new - - def build_df(data, LAMBDA, K): - df = pd.DataFrame(data) - df['s_name'] = df['s_name'].apply(fix_sname) - setattr(df, 'LAMBDA', LAMBDA) - setattr(df, 'K', K) - df['bitscore'] = (LAMBDA * df['score'] - np.log(K)) / np.log(2) - return df - - data = [] - LAMBDA = None - K = None - with open(fn) as fp: - while (True): - try: - line = fp.next().strip() - except StopIteration: - break - if not line: - continue - if line.startswith('#'): - if 'lambda' in line: - meta = line.strip(' #').split() - meta = {k:v for k, _, v in map(lambda x: x.partition('='), meta)} - LAMBDA = float(meta['lambda']) - K = float(meta['K']) - else: - continue - if line.startswith('a'): - cur_aln = {} - - # Alignment info - tokens = line.split() - for token in tokens[1:]: - key, _, val = token.strip().partition('=') - cur_aln[key] = float(val) - - # First sequence info - line = fp.next() - tokens = line.split() - cur_aln['s_name'] = tokens[1] - cur_aln['s_start'] = int(tokens[2]) - cur_aln['s_aln_len'] = int(tokens[3]) - cur_aln['s_strand'] = tokens[4] - cur_aln['s_len'] = int(tokens[5]) - - # First sequence info - line = fp.next() - tokens = line.split() - cur_aln['q_name'] = tokens[1] - cur_aln['q_start'] = int(tokens[2]) - cur_aln['q_aln_len'] = int(tokens[3]) - cur_aln['q_strand'] = tokens[4] - cur_aln['q_len'] = int(tokens[5]) - - data.append(cur_aln) - if len(data) >= chunksize: - if LAMBDA is None: - raise Exception("old version of lastal; please update") - yield build_df(data, LAMBDA, K) - data = [] - - if data: - yield build_df(data, LAMBDA, K) - diff --git a/dammit/report.py b/dammit/report.py index 24a37d14..c3559b19 100644 --- a/dammit/report.py +++ b/dammit/report.py @@ -18,7 +18,6 @@ def get_report_tasks(transcriptome, annotator, databases, n_threads=1): tasks = [] outputs = [] - orthodb_best_hits = annotator.orthodb_fn + '.best.csv' orthodb_gff3 = annotator.orthodb_fn + '.gff3' tasks.append( @@ -44,7 +43,7 @@ def get_report_tasks(transcriptome, annotator, databases, n_threads=1): pfam_gff3 = annotator.pfam_fn + '.gff3' tasks.append( get_hmmscan_gff3_task(annotator.pfam_fn, - pfam_gff3, + pfam_gff3, 'Pfam') ) outputs.append(pfam_gff3) @@ -52,7 +51,7 @@ def get_report_tasks(transcriptome, annotator, databases, n_threads=1): rfam_gff3 = annotator.rfam_fn + '.gff3' tasks.append( get_cmscan_gff3_task(annotator.rfam_fn, - rfam_gff3, + rfam_gff3, 'Rfam') ) outputs.append(rfam_gff3) @@ -66,18 +65,13 @@ def get_report_tasks(transcriptome, annotator, databases, n_threads=1): outputs.append(annotator.transdecoder_gff3_fn) - merged_gff3 = transcriptome + '.dammit.gff3' tasks.append( - get_gff3_merge_task(outputs, merged_gff3) + get_gff3_merge_task(outputs, annotator.final_gff3_fn) ) - outputs.append(merged_gff3) - renamed_transcriptome = transcriptome + '.dammit.fasta' tasks.append( - get_annotate_fasta_task(transcriptome, merged_gff3, - renamed_transcriptome) + get_annotate_fasta_task(transcriptome, annotator.final_gff3_fn, + annotator.final_fasta_fn) ) - outputs.append(renamed_transcriptome) - - return outputs, tasks + return tasks diff --git a/dammit/tasks.py b/dammit/tasks.py index fa404897..0da1c433 100644 --- a/dammit/tasks.py +++ b/dammit/tasks.py @@ -71,10 +71,10 @@ def get_group_task(group_name, tasks): @create_task_object -def get_download_task(url, target_fn, label='default'): +def get_download_task(url, target_fn): cmd = 'curl -o {target_fn} {url}'.format(**locals()) - name = '_'.join(['download_gunzip', target_fn, label]) + name = 'download_' + os.path.basename(target_fn) return {'title': title_with_actions, 'name': name, @@ -150,7 +150,7 @@ def fix(): @create_task_object -def get_rename_transcriptome_task(transcriptome_fn, output_fn, names_fn, +def get_rename_transcriptome_task(transcriptome_fn, output_fn, names_fn, transcript_basename, split_regex=None): import re @@ -184,7 +184,7 @@ def fix(): 'actions': [fix], 'targets': [output_fn, names_fn], 'file_dep': [transcriptome_fn], - 'clean': [clean_targets]} + 'clean': [clean_targets]} @create_task_object @@ -242,7 +242,7 @@ def get_lastdb_task(db_fn, db_out_prefix, lastdb_cfg, prot=True): Returns: dict: A pydoit task. ''' - + exc = which('lastdb') params = lastdb_cfg['params'] if prot: @@ -299,10 +299,11 @@ def get_lastal_task(query, db, out_fn, translate, cutoff, n_threads, lastal_cfg) def get_maf_best_hits_task(maf_fn, output_fn): hits_mgr = BestHits() + from .fileio import maf def cmd(): df = pd.concat([group for group in - parsers.maf_to_df_iter(maf_fn)]) + maf.MafParser(maf_fn)]) df = hits_mgr.best_hits(df) df.to_csv(output_fn, index=False) @@ -347,13 +348,15 @@ def get_cat_task(file_list, target_fn): @create_task_object def get_busco_task(input_filename, output_name, busco_db_dir, input_type, n_threads, busco_cfg): - + name = 'busco:' + os.path.basename(input_filename) + '-' + os.path.basename(busco_db_dir) assert input_type in ['genome', 'OGS', 'trans'] exc = which('BUSCO_v1.1b1.py') # BUSCO chokes on file paths as output names output_name = os.path.basename(output_name) + summary_fn = os.path.join('run_{0}'.format(output_name), + 'short_summary_{0}'.format(output_name)) cmd = 'python3 {exc} -in {input_filename} -f -o {output_name} -l {busco_db_dir} '\ '-m {input_type} -c {n_threads}'.format(**locals()) @@ -361,6 +364,7 @@ def get_busco_task(input_filename, output_name, busco_db_dir, input_type, return {'name': name, 'title': title_with_actions, 'actions': [cmd], + 'targets': [summary_fn], 'file_dep': [input_filename], 'uptodate': [run_once], 'clean': [(clean_folder, ['run_' + output_name])]} @@ -381,12 +385,12 @@ def get_cmpress_task(db_filename, infernal_cfg): @create_task_object -def get_cmscan_task(input_filename, output_filename, db_filename, +def get_cmscan_task(input_filename, output_filename, db_filename, cutoff, n_threads, infernal_cfg): - + name = 'cmscan:' + os.path.basename(input_filename) + '.x.' + \ os.path.basename(db_filename) - + exc = which('cmscan') cmd = '{exc} --cpu {n_threads} --rfam --nohmmonly -E {cutoff}'\ ' --tblout {output_filename} {db_filename} {input_filename}'\ @@ -402,7 +406,7 @@ def get_cmscan_task(input_filename, output_filename, db_filename, @create_task_object def get_hmmpress_task(db_filename, hmmer_cfg): - + name = 'hmmpress:' + os.path.basename(db_filename) exc = which('hmmpress') cmd = '{exc} {db_filename}'.format(**locals()) @@ -416,12 +420,12 @@ def get_hmmpress_task(db_filename, hmmer_cfg): @create_task_object -def get_hmmscan_task(input_filename, output_filename, db_filename, +def get_hmmscan_task(input_filename, output_filename, db_filename, cutoff, n_threads, hmmer_cfg): name = 'hmmscan:' + os.path.basename(input_filename) + '.x.' + \ os.path.basename(db_filename) - + exc = which('hmmscan') stat = output_filename + '.out' cmd = '{exc} --cpu {n_threads} --domtblout {output_filename} -E {cutoff}'\ @@ -491,34 +495,34 @@ def get_transdecoder_predict_task(input_filename, db_filename, transdecoder_cfg) exc = which('TransDecoder.Predict') cmd = '{exc} -t {input_filename} --retain_pfam_hits {db_filename} \ --retain_long_orfs {orf_cutoff}'.format(**locals()) - + return {'name': name, 'title': title_with_actions, 'actions': [cmd], - 'file_dep': [input_filename, + 'file_dep': [input_filename, input_filename + '.transdecoder_dir/longest_orfs.pep', db_filename], 'targets': [input_filename + '.transdecoder' + ext \ for ext in ['.bed', '.cds', '.pep', '.gff3', '.mRNA']], - 'clean': [clean_targets, + 'clean': [clean_targets, (clean_folder, [input_filename + '.transdecoder_dir'])]} @create_task_object def get_maf_gff3_task(input_filename, output_filename, database): + from .fileio import maf name = 'maf-gff3:' + os.path.basename(output_filename) def cmd(): if input_filename.endswith('.csv') or input_filename.endswith('.tsv'): it = pd.read_csv(input_filename, chunksize=10000) else: - it = parsers.maf_to_df_iter(input_filename) + it = maf.MafParser(input_filename).__iter__() with open(output_filename, 'a') as fp: for group in it: - gff_group = gff.maf_to_gff3_df(group, 'dammit.last', - database) + gff_group = gff.maf_to_gff3_df(group, database=database) gff.write_gff3_df(gff_group, fp) return {'name': name, @@ -539,7 +543,7 @@ def cmd(): with open(output_filename, 'a') as fp: for group in parsers.crb_to_df_iter(input_filename, remap=True): - gff_group = gff.crb_to_gff3_df(group, 'dammit.crbb', database) + gff_group = gff.crb_to_gff3_df(group, database=database) gff.write_gff3_df(gff_group, fp) return {'name': name, @@ -559,8 +563,7 @@ def get_hmmscan_gff3_task(input_filename, output_filename, database): def cmd(): with open(output_filename, 'a') as fp: for group in pd.read_csv(input_filename, chunksize=10000): - gff_group = gff.hmmscan_to_gff3_df(group, 'dammit.hmmscan', - database) + gff_group = gff.hmmscan_to_gff3_df(group, database=database) gff.write_gff3_df(gff_group, fp) return {'name': name, @@ -580,8 +583,7 @@ def get_cmscan_gff3_task(input_filename, output_filename, database): def cmd(): with open(output_filename, 'a') as fp: for group in parsers.cmscan_to_df_iter(input_filename): - gff_group = gff.cmscan_to_gff3_df(group, 'dammit.cmscan', - database) + gff_group = gff.cmscan_to_gff3_df(group, database=database) gff.write_gff3_df(gff_group, fp) return {'name': name, @@ -609,16 +611,33 @@ def get_gff3_merge_task(gff3_filenames, output_filename): 'targets': [output_filename], 'clean': [clean_targets]} +''' +@create_task_object +def get_json_task(gff3_fn, fasta_fn, output_fn): + + import json + + def create_json(): + annotations = pd.concat(parsers.parse_gff3(gff3_fn)) + + with open(output_fn, 'wb') as fp: + fp.write('{\n') + for record in ReadParser(fasta_fn): + short_name, _, _ = record.name.split()[0] + annots = annotations.query('seqid == "{0}"'.format(short_name)) +''' + @create_task_object def get_annotate_fasta_task(transcriptome_fn, gff3_fn, output_fn): - + name = 'fasta-annotate:{0}'.format(output_fn) def annotate_fasta(): annotations = pd.concat([g for g in \ parsers.parse_gff3(gff3_fn)]) with open(output_fn, 'wb') as fp: + for n, record in enumerate(ReadParser(transcriptome_fn)): df = annotations.query('seqid == "{0}"'.format(record.name)) annots = ['len={0}'.format(len(record.sequence))] @@ -630,8 +649,8 @@ def annotate_fasta(): 'protein_hmm_match', 'RNA_sequence_secondary_structure']: - collapsed = ','.join(['{}:{}-{}'.format(row.Name.split(':dammit')[0], - int(row.start), + collapsed = ','.join(['{}:{}-{}'.format(row.Name.split(':dammit')[0], + int(row.start), int(row.end)) \ for _, row in fgroup.iterrows()]) if feature_type == 'translated_nucleotide_match': @@ -643,9 +662,9 @@ def annotate_fasta(): annots.append('{0}={1}'.format(key, collapsed)) elif feature_type in ['exon', 'CDS', 'gene', - 'five_prime_UTR', 'three_prime_UTR', + 'five_prime_UTR', 'three_prime_UTR', 'mRNA']: - + collapsed = ','.join(['{}-{}'.format(int(row.start), int(row.end)) \ for _, row in fgroup.iterrows()]) @@ -664,29 +683,33 @@ def annotate_fasta(): @create_task_object -def get_transcriptome_stats_task(transcriptome, output_fn): +def get_summary_task(transcriptome, name_map_fn, work_dir, final_gff3_fn, + busco_summary_fn, output_fn, transcript_info_fn): - name = 'transcriptome_stats:' + os.path.basename(transcriptome) + name = 'json_summary:' + os.path.basename(transcriptome) K = 25 - + def parse(fn): hll = HLLCounter(.01, K) lens = [] names = [] + annots = [] gc_len = 0 for contig in ReadParser(fn): lens.append(len(contig.sequence)) - names.append(contig.name) + name, _, annot = contig.name.partition(' ') + names.append(name) + annots.append(annot) hll.consume_string(contig.sequence) gc_len += contig.sequence.count('C') gc_len += contig.sequence.count('G') - S = pd.Series(lens, index=names) + df = pd.DataFrame({'length': lens, 'annotations': annots}, index=names) try: - S.sort_values() + df.sort_values('length', inplace=True) except AttributeError: - S.sort() - gc_perc = float(gc_len) / S.sum() - return S, hll.estimate_cardinality(), gc_perc + df.sort('length', inplace=True) + gc_perc = float(gc_len) / df['length'].sum() + return df, hll.estimate_cardinality(), gc_perc def calc_NX(lens, X): N = lens.sum() @@ -703,34 +726,93 @@ def calc_NX(lens, X): break return NXlen, NXpos - def cmd(): - lens, uniq_kmers, gc_perc = parse(transcriptome) - - exp_kmers = (lens - (K+1)).sum() + def stats(): + len_df, uniq_kmers, gc_perc = parse(transcriptome) + + exp_kmers = (len_df['length'] - (K+1)).sum() redundancy = float(exp_kmers - uniq_kmers) / exp_kmers if redundancy < 0: redundancy = 0.0 - N50len, N50pos = calc_NX(lens, 50) - stats = {'N': len(lens), - 'sum': lens.sum(), - 'min': lens.min(), - 'max': lens.max(), - 'med': lens.median(), - 'mean': lens.mean(), + N50len, N50pos = calc_NX(len_df['length'], 50) + data = {'N': len(len_df), + 'sum': len_df['length'].sum(), + 'min': len_df['length'].min(), + 'max': len_df['length'].max(), + 'med': len_df['length'].median(), + 'mean': len_df['length'].mean(), 'N50len': N50len, 'N50pos': N50pos, '25_mers': exp_kmers, '25_mers_unique': uniq_kmers, 'redundancy': redundancy, 'GCperc': gc_perc} - + return len_df, data + + def cmd(): + len_df, stats_summary = stats() + name_map_df = pd.read_csv(name_map_fn) + busco_summary = parsers.parse_busco_summary(busco_summary_fn) + transcript_df = pd.merge(name_map_df, len_df, + left_on='renamed', + right_index=True).rename(columns={'renamed': 'seqid'}) + + + run_summary = {'work_dir': work_dir, + 'fasta': transcriptome, + 'gff3': final_gff3_fn, + 'busco': busco_summary, + 'stats': stats_summary} + # + transcript_df.to_csv(transcript_info_fn) + with open(output_fn, 'wb') as fp: - json.dump(stats, fp, indent=4) + json.dump(run_summary, fp, indent=4) return {'name': name, 'title': title_with_actions, 'actions': [(cmd, [])], - 'file_dep': [transcriptome], - 'targets': [output_fn], + 'file_dep': [transcriptome, + name_map_fn, + final_gff3_fn, + busco_summary_fn], + 'targets': [output_fn, transcript_info_fn], + 'clean': [clean_targets]} + + +@create_task_object +def get_create_zodb_task(gff3_fn, transcript_info_fn, database_fn): + from ZODB import FileStorage, DB + from flask.ext.zodb import Dict as zdict + import transaction + + name = 'create-zodb:' + os.path.basename(database_fn) + + def cmd(): + db = DB(FileStorage.FileStorage(database_fn)) + con = db.open() + root = con.root() + + root['transcripts'] = zdict() + + def func(row): + root['transcripts'][row.seqid] = zdict() + root['transcripts'][row.seqid]['length'] = row.length + root['transcripts'][row.seqid]['short_annot'] = row.annotations + transaction.commit() + transcript_df = pd.read_csv(transcript_info_fn).apply(func, axis=1) + + gff3_df = pd.concat(parsers.parse_gff3(gff3_fn)) + for transcript, group in gff3_df.groupby('seqid'): + root['transcripts'][transcript]['annotations'] = group + transaction.commit() + + con.close() + + return {'name': name, + 'title': title_with_actions, + 'actions': ['rm -f {0}'.format(database_fn), + cmd], + 'file_dep': [gff3_fn, transcript_info_fn], + 'targets': [database_fn], 'clean': [clean_targets]} diff --git a/dammit/tests/test_main.py b/dammit/tests/test_main.py index 0da5c50a..610534e3 100644 --- a/dammit/tests/test_main.py +++ b/dammit/tests/test_main.py @@ -80,7 +80,7 @@ def test_dammit_databases_check_fail(self): ''' with TemporaryDirectory() as td: - + args = ['databases', '--database-dir', td] status, out, err = run(args, fail_ok=True) self.assertIn('prep incomplete', err) @@ -143,7 +143,7 @@ def test_annotate_evalue(self): self.assertEquals(open(gff3_fn).read(), open(exp_gff3).read()) self.assertEquals(open(fasta_fn).read(), open(exp_fasta).read()) - + def test_annotate_outdir(self): '''Test that the --output-dir argument works. @@ -177,9 +177,12 @@ def test_annotate_dbdir(self): with TemporaryDirectory() as td,\ TestData('pom.single.fa', td) as transcripts: - db_dir = os.environ['DAMMIT_DB_DIR'] - args = ['annotate', transcripts, '--database-dir', db_dir] - status, out, err = run(args, in_directory=td) + try: + db_dir = os.environ['DAMMIT_DB_DIR'] + args = ['annotate', transcripts, '--database-dir', db_dir] + status, out, err = run(args, in_directory=td) + except KeyError: + pass def test_annotate_user_databases(self): '''Test that a user database works. @@ -252,7 +255,7 @@ def test_check_system_path_alldeps(self): self.assertIn(name, names) if name != 'LAST': self.assertTrue(stat, msg=name + ' ' + msg) - + def test_handle_nodeps(self): os.environ['PATH'] = '' handler = dependencies.DependencyHandler() @@ -273,6 +276,3 @@ class TestDatabases(TestCase): @classmethod def setup_class(cls): cls.db_dir = 'test_db_dir' - - - diff --git a/dammit/viewer/__init__.py b/dammit/viewer/__init__.py new file mode 100644 index 00000000..d9801610 --- /dev/null +++ b/dammit/viewer/__init__.py @@ -0,0 +1,5 @@ +import os +from .. import common + +static_folder = os.path.join(common.rel_path, 'viewer', 'static') +template_folder = os.path.join(common.rel_path, 'viewer', 'templates') diff --git a/dammit/viewer/database.py b/dammit/viewer/database.py new file mode 100644 index 00000000..e7e2e9b0 --- /dev/null +++ b/dammit/viewer/database.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +from __future__ import print_function + +from flask import g, current_app +from flask.ext.zodb import ZODB + +db = ZODB() diff --git a/dammit/viewer/genomed3plot.py b/dammit/viewer/genomed3plot.py new file mode 100644 index 00000000..cf500290 --- /dev/null +++ b/dammit/viewer/genomed3plot.py @@ -0,0 +1,46 @@ +import pandas as pd + +sources = ['HMMER', + 'LAST', + 'transdecoder', + 'crb-blast', + 'Infernal'] + +def create_tracks(transcript, transcript_df, track_start=100, track_width=40, track_sep=10): + tracks = [] + for source in sources: + if source in transcript_df.source.values: + source_df = transcript_df.query('source == "{0}"'.format(source)) + track = {} + track['trackName'] = source + track['trackType'] = "track" + track['trackFeatures'] = "complex" + track['visible'] = True + track['min_slice'] = True + track['showTooltip'] = True + + track['inner_radius'] = track_start + track['outer_radius'] = track_start + track_width + + items = [] + id_count = 0 + for _, row in source_df.iterrows(): + + if row.notnull().Note: + name = row.Note + elif row.notnull().Name: + name = row.Name + else: + name = row.ID + + items.append({'id': id_count, + 'start': int(row.start), + 'end': int(row.end), + 'name': name + }) + id_count += 1 + track['items'] = items + tracks.append(track) + track_start += track_width + track_sep + + return tracks diff --git a/dammit/viewer/static/FileSaver.js b/dammit/viewer/static/FileSaver.js new file mode 100644 index 00000000..378a9dcc --- /dev/null +++ b/dammit/viewer/static/FileSaver.js @@ -0,0 +1,232 @@ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 2013-10-21 + * + * By Eli Grey, http://eligrey.com + * License: X11/MIT + * See LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, + plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs + || (typeof navigator !== 'undefined' && navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator)) + || (function(view) { + "use strict"; + var + doc = view.document + // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , URL = view.URL || view.webkitURL || view + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = !view.externalHost && "download" in save_link + , click = function(node) { + var event = doc.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, false, view, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + node.dispatchEvent(event); + } + , webkit_req_fs = view.webkitRequestFileSystem + , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem + , throw_outside = function (ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + , fs_min_size = 0 + , deletion_queue = [] + , process_deletion_queue = function() { + var i = deletion_queue.length; + while (i--) { + var file = deletion_queue[i]; + if (typeof file === "string") { // file is an object URL + URL.revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + } + deletion_queue.length = 0; // clear queue + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , FileSaver = function(blob, name) { + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , blob_changed = false + , object_url + , target_view + , get_object_url = function() { + var object_url = get_URL().createObjectURL(blob); + deletion_queue.push(object_url); + return object_url; + } + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + // don't create more object URLs than needed + if (blob_changed || !object_url) { + object_url = get_object_url(blob); + } + if (target_view) { + target_view.location.href = object_url; + } else { + window.open(object_url, "_blank"); + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + } + , abortable = function(func) { + return function() { + if (filesaver.readyState !== filesaver.DONE) { + return func.apply(this, arguments); + } + }; + } + , create_if_not_found = {create: true, exclusive: false} + , slice + ; + filesaver.readyState = filesaver.INIT; + if (!name) { + name = "download"; + } + if (can_use_save_link) { + object_url = get_object_url(blob); + // FF for Android has a nasty garbage collection mechanism + // that turns all objects that are not pure javascript into 'deadObject' + // this means `doc` and `save_link` are unusable and need to be recreated + // `view` is usable though: + doc = view.document; + save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a"); + save_link.href = object_url; + save_link.download = name; + var event = doc.createEvent("MouseEvents"); + event.initMouseEvent( + "click", true, false, view, 0, 0, 0, 0, 0 + , false, false, false, false, 0, null + ); + save_link.dispatchEvent(event); + filesaver.readyState = filesaver.DONE; + dispatch_all(); + return; + } + // Object and web filesystem URLs have a problem saving in Google Chrome when + // viewed in a tab, so I force save with application/octet-stream + // http://code.google.com/p/chromium/issues/detail?id=91158 + if (view.chrome && type && type !== force_saveable_type) { + slice = blob.slice || blob.webkitSlice; + blob = slice.call(blob, 0, blob.size, force_saveable_type); + blob_changed = true; + } + // Since I can't be sure that the guessed media type will trigger a download + // in WebKit, I append .download to the filename. + // https://bugs.webkit.org/show_bug.cgi?id=65440 + if (webkit_req_fs && name !== "download") { + name += ".download"; + } + if (type === force_saveable_type || webkit_req_fs) { + target_view = view; + } + if (!req_fs) { + fs_error(); + return; + } + fs_min_size += blob.size; + req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) { + fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) { + var save = function() { + dir.getFile(name, create_if_not_found, abortable(function(file) { + file.createWriter(abortable(function(writer) { + writer.onwriteend = function(event) { + target_view.location.href = file.toURL(); + deletion_queue.push(file); + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "writeend", event); + }; + writer.onerror = function() { + var error = writer.error; + if (error.code !== error.ABORT_ERR) { + fs_error(); + } + }; + "writestart progress write abort".split(" ").forEach(function(event) { + writer["on" + event] = filesaver["on" + event]; + }); + writer.write(blob); + filesaver.abort = function() { + writer.abort(); + filesaver.readyState = filesaver.DONE; + }; + filesaver.readyState = filesaver.WRITING; + }), fs_error); + }), fs_error); + }; + dir.getFile(name, {create: false}, abortable(function(file) { + // delete file if it already exists + file.remove(); + save(); + }), abortable(function(ex) { + if (ex.code === ex.NOT_FOUND_ERR) { + save(); + } else { + fs_error(); + } + })); + }), fs_error); + }), fs_error); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name) { + return new FileSaver(blob, name); + } + ; + FS_proto.abort = function() { + var filesaver = this; + filesaver.readyState = filesaver.DONE; + dispatch(filesaver, "abort"); + }; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + view.addEventListener("unload", process_deletion_queue, false); + return saveAs; +}(this.self || this.window || this.content)); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== 'undefined') module.exports = saveAs; diff --git a/dammit/viewer/static/StackBlur.js b/dammit/viewer/static/StackBlur.js new file mode 100644 index 00000000..7dea6d04 --- /dev/null +++ b/dammit/viewer/static/StackBlur.js @@ -0,0 +1,611 @@ +/* + +StackBlur - a fast almost Gaussian Blur For Canvas + +Version: 0.5 +Author: Mario Klingemann +Contact: mario@quasimondo.com +Website: http://www.quasimondo.com/StackBlurForCanvas +Twitter: @quasimondo + +In case you find this class useful - especially in commercial projects - +I am not totally unhappy for a small donation to my PayPal account +mario@quasimondo.de + +Or support me on flattr: +https://flattr.com/thing/72791/StackBlur-a-fast-almost-Gaussian-Blur-Effect-for-CanvasJavascript + +Copyright (c) 2010 Mario Klingemann + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +*/ + +var mul_table = [ + 512,512,456,512,328,456,335,512,405,328,271,456,388,335,292,512, + 454,405,364,328,298,271,496,456,420,388,360,335,312,292,273,512, + 482,454,428,405,383,364,345,328,312,298,284,271,259,496,475,456, + 437,420,404,388,374,360,347,335,323,312,302,292,282,273,265,512, + 497,482,468,454,441,428,417,405,394,383,373,364,354,345,337,328, + 320,312,305,298,291,284,278,271,265,259,507,496,485,475,465,456, + 446,437,428,420,412,404,396,388,381,374,367,360,354,347,341,335, + 329,323,318,312,307,302,297,292,287,282,278,273,269,265,261,512, + 505,497,489,482,475,468,461,454,447,441,435,428,422,417,411,405, + 399,394,389,383,378,373,368,364,359,354,350,345,341,337,332,328, + 324,320,316,312,309,305,301,298,294,291,287,284,281,278,274,271, + 268,265,262,259,257,507,501,496,491,485,480,475,470,465,460,456, + 451,446,442,437,433,428,424,420,416,412,408,404,400,396,392,388, + 385,381,377,374,370,367,363,360,357,354,350,347,344,341,338,335, + 332,329,326,323,320,318,315,312,310,307,304,302,299,297,294,292, + 289,287,285,282,280,278,275,273,271,269,267,265,263,261,259]; + + +var shg_table = [ + 9, 11, 12, 13, 13, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 17, + 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, + 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, + 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, + 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24 ]; + +function stackBlurImage( imageID, canvasID, radius, blurAlphaChannel ) +{ + + var img = document.getElementById( imageID ); + var w = img.naturalWidth; + var h = img.naturalHeight; + + var canvas = document.getElementById( canvasID ); + + canvas.style.width = w + "px"; + canvas.style.height = h + "px"; + canvas.width = w; + canvas.height = h; + + var context = canvas.getContext("2d"); + context.clearRect( 0, 0, w, h ); + context.drawImage( img, 0, 0 ); + + if ( isNaN(radius) || radius < 1 ) return; + + if ( blurAlphaChannel ) + stackBlurCanvasRGBA( canvasID, 0, 0, w, h, radius ); + else + stackBlurCanvasRGB( canvasID, 0, 0, w, h, radius ); +} + + +function stackBlurCanvasRGBA( id, top_x, top_y, width, height, radius ) +{ + if ( isNaN(radius) || radius < 1 ) return; + radius |= 0; + + var canvas = document.getElementById( id ); + var context = canvas.getContext("2d"); + var imageData; + + try { + try { + imageData = context.getImageData( top_x, top_y, width, height ); + } catch(e) { + + // NOTE: this part is supposedly only needed if you want to work with local files + // so it might be okay to remove the whole try/catch block and just use + // imageData = context.getImageData( top_x, top_y, width, height ); + try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); + imageData = context.getImageData( top_x, top_y, width, height ); + } catch(e) { + alert("Cannot access local image"); + throw new Error("unable to access local image data: " + e); + return; + } + } + } catch(e) { + alert("Cannot access image"); + throw new Error("unable to access image data: " + e); + } + + var pixels = imageData.data; + + var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, a_sum, + r_out_sum, g_out_sum, b_out_sum, a_out_sum, + r_in_sum, g_in_sum, b_in_sum, a_in_sum, + pr, pg, pb, pa, rbs; + + var div = radius + radius + 1; + var w4 = width << 2; + var widthMinus1 = width - 1; + var heightMinus1 = height - 1; + var radiusPlus1 = radius + 1; + var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2; + + var stackStart = new BlurStack(); + var stack = stackStart; + for ( i = 1; i < div; i++ ) + { + stack = stack.next = new BlurStack(); + if ( i == radiusPlus1 ) var stackEnd = stack; + } + stack.next = stackStart; + var stackIn = null; + var stackOut = null; + + yw = yi = 0; + + var mul_sum = mul_table[radius]; + var shg_sum = shg_table[radius]; + + for ( y = 0; y < height; y++ ) + { + r_in_sum = g_in_sum = b_in_sum = a_in_sum = r_sum = g_sum = b_sum = a_sum = 0; + + r_out_sum = radiusPlus1 * ( pr = pixels[yi] ); + g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] ); + b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] ); + a_out_sum = radiusPlus1 * ( pa = pixels[yi+3] ); + + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + a_sum += sumFactor * pa; + + stack = stackStart; + + for( i = 0; i < radiusPlus1; i++ ) + { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + for( i = 1; i < radiusPlus1; i++ ) + { + p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); + r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i ); + g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs; + b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs; + a_sum += ( stack.a = ( pa = pixels[p+3])) * rbs; + + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + a_in_sum += pa; + + stack = stack.next; + } + + + stackIn = stackStart; + stackOut = stackEnd; + for ( x = 0; x < width; x++ ) + { + pixels[yi+3] = pa = (a_sum * mul_sum) >> shg_sum; + if ( pa != 0 ) + { + pa = 255 / pa; + pixels[yi] = ((r_sum * mul_sum) >> shg_sum) * pa; + pixels[yi+1] = ((g_sum * mul_sum) >> shg_sum) * pa; + pixels[yi+2] = ((b_sum * mul_sum) >> shg_sum) * pa; + } else { + pixels[yi] = pixels[yi+1] = pixels[yi+2] = 0; + } + + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + a_sum -= a_out_sum; + + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + a_out_sum -= stackIn.a; + + p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; + + r_in_sum += ( stackIn.r = pixels[p]); + g_in_sum += ( stackIn.g = pixels[p+1]); + b_in_sum += ( stackIn.b = pixels[p+2]); + a_in_sum += ( stackIn.a = pixels[p+3]); + + r_sum += r_in_sum; + g_sum += g_in_sum; + b_sum += b_in_sum; + a_sum += a_in_sum; + + stackIn = stackIn.next; + + r_out_sum += ( pr = stackOut.r ); + g_out_sum += ( pg = stackOut.g ); + b_out_sum += ( pb = stackOut.b ); + a_out_sum += ( pa = stackOut.a ); + + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + a_in_sum -= pa; + + stackOut = stackOut.next; + + yi += 4; + } + yw += width; + } + + + for ( x = 0; x < width; x++ ) + { + g_in_sum = b_in_sum = a_in_sum = r_in_sum = g_sum = b_sum = a_sum = r_sum = 0; + + yi = x << 2; + r_out_sum = radiusPlus1 * ( pr = pixels[yi]); + g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]); + b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]); + a_out_sum = radiusPlus1 * ( pa = pixels[yi+3]); + + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + a_sum += sumFactor * pa; + + stack = stackStart; + + for( i = 0; i < radiusPlus1; i++ ) + { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack.a = pa; + stack = stack.next; + } + + yp = width; + + for( i = 1; i <= radius; i++ ) + { + yi = ( yp + x ) << 2; + + r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i ); + g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs; + b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs; + a_sum += ( stack.a = ( pa = pixels[yi+3])) * rbs; + + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + a_in_sum += pa; + + stack = stack.next; + + if( i < heightMinus1 ) + { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for ( y = 0; y < height; y++ ) + { + p = yi << 2; + pixels[p+3] = pa = (a_sum * mul_sum) >> shg_sum; + if ( pa > 0 ) + { + pa = 255 / pa; + pixels[p] = ((r_sum * mul_sum) >> shg_sum ) * pa; + pixels[p+1] = ((g_sum * mul_sum) >> shg_sum ) * pa; + pixels[p+2] = ((b_sum * mul_sum) >> shg_sum ) * pa; + } else { + pixels[p] = pixels[p+1] = pixels[p+2] = 0; + } + + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + a_sum -= a_out_sum; + + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + a_out_sum -= stackIn.a; + + p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; + + r_sum += ( r_in_sum += ( stackIn.r = pixels[p])); + g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1])); + b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2])); + a_sum += ( a_in_sum += ( stackIn.a = pixels[p+3])); + + stackIn = stackIn.next; + + r_out_sum += ( pr = stackOut.r ); + g_out_sum += ( pg = stackOut.g ); + b_out_sum += ( pb = stackOut.b ); + a_out_sum += ( pa = stackOut.a ); + + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + a_in_sum -= pa; + + stackOut = stackOut.next; + + yi += width; + } + } + + context.putImageData( imageData, top_x, top_y ); + +} + + +function stackBlurCanvasRGB( id, top_x, top_y, width, height, radius ) +{ + if ( isNaN(radius) || radius < 1 ) return; + radius |= 0; + + var canvas = document.getElementById( id ); + var context = canvas.getContext("2d"); + var imageData; + + try { + try { + imageData = context.getImageData( top_x, top_y, width, height ); + } catch(e) { + + // NOTE: this part is supposedly only needed if you want to work with local files + // so it might be okay to remove the whole try/catch block and just use + // imageData = context.getImageData( top_x, top_y, width, height ); + try { + netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); + imageData = context.getImageData( top_x, top_y, width, height ); + } catch(e) { + alert("Cannot access local image"); + throw new Error("unable to access local image data: " + e); + return; + } + } + } catch(e) { + alert("Cannot access image"); + throw new Error("unable to access image data: " + e); + } + + var pixels = imageData.data; + + var x, y, i, p, yp, yi, yw, r_sum, g_sum, b_sum, + r_out_sum, g_out_sum, b_out_sum, + r_in_sum, g_in_sum, b_in_sum, + pr, pg, pb, rbs; + + var div = radius + radius + 1; + var w4 = width << 2; + var widthMinus1 = width - 1; + var heightMinus1 = height - 1; + var radiusPlus1 = radius + 1; + var sumFactor = radiusPlus1 * ( radiusPlus1 + 1 ) / 2; + + var stackStart = new BlurStack(); + var stack = stackStart; + for ( i = 1; i < div; i++ ) + { + stack = stack.next = new BlurStack(); + if ( i == radiusPlus1 ) var stackEnd = stack; + } + stack.next = stackStart; + var stackIn = null; + var stackOut = null; + + yw = yi = 0; + + var mul_sum = mul_table[radius]; + var shg_sum = shg_table[radius]; + + for ( y = 0; y < height; y++ ) + { + r_in_sum = g_in_sum = b_in_sum = r_sum = g_sum = b_sum = 0; + + r_out_sum = radiusPlus1 * ( pr = pixels[yi] ); + g_out_sum = radiusPlus1 * ( pg = pixels[yi+1] ); + b_out_sum = radiusPlus1 * ( pb = pixels[yi+2] ); + + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + + stack = stackStart; + + for( i = 0; i < radiusPlus1; i++ ) + { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack = stack.next; + } + + for( i = 1; i < radiusPlus1; i++ ) + { + p = yi + (( widthMinus1 < i ? widthMinus1 : i ) << 2 ); + r_sum += ( stack.r = ( pr = pixels[p])) * ( rbs = radiusPlus1 - i ); + g_sum += ( stack.g = ( pg = pixels[p+1])) * rbs; + b_sum += ( stack.b = ( pb = pixels[p+2])) * rbs; + + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + + stack = stack.next; + } + + + stackIn = stackStart; + stackOut = stackEnd; + for ( x = 0; x < width; x++ ) + { + pixels[yi] = (r_sum * mul_sum) >> shg_sum; + pixels[yi+1] = (g_sum * mul_sum) >> shg_sum; + pixels[yi+2] = (b_sum * mul_sum) >> shg_sum; + + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + + p = ( yw + ( ( p = x + radius + 1 ) < widthMinus1 ? p : widthMinus1 ) ) << 2; + + r_in_sum += ( stackIn.r = pixels[p]); + g_in_sum += ( stackIn.g = pixels[p+1]); + b_in_sum += ( stackIn.b = pixels[p+2]); + + r_sum += r_in_sum; + g_sum += g_in_sum; + b_sum += b_in_sum; + + stackIn = stackIn.next; + + r_out_sum += ( pr = stackOut.r ); + g_out_sum += ( pg = stackOut.g ); + b_out_sum += ( pb = stackOut.b ); + + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + + stackOut = stackOut.next; + + yi += 4; + } + yw += width; + } + + + for ( x = 0; x < width; x++ ) + { + g_in_sum = b_in_sum = r_in_sum = g_sum = b_sum = r_sum = 0; + + yi = x << 2; + r_out_sum = radiusPlus1 * ( pr = pixels[yi]); + g_out_sum = radiusPlus1 * ( pg = pixels[yi+1]); + b_out_sum = radiusPlus1 * ( pb = pixels[yi+2]); + + r_sum += sumFactor * pr; + g_sum += sumFactor * pg; + b_sum += sumFactor * pb; + + stack = stackStart; + + for( i = 0; i < radiusPlus1; i++ ) + { + stack.r = pr; + stack.g = pg; + stack.b = pb; + stack = stack.next; + } + + yp = width; + + for( i = 1; i <= radius; i++ ) + { + yi = ( yp + x ) << 2; + + r_sum += ( stack.r = ( pr = pixels[yi])) * ( rbs = radiusPlus1 - i ); + g_sum += ( stack.g = ( pg = pixels[yi+1])) * rbs; + b_sum += ( stack.b = ( pb = pixels[yi+2])) * rbs; + + r_in_sum += pr; + g_in_sum += pg; + b_in_sum += pb; + + stack = stack.next; + + if( i < heightMinus1 ) + { + yp += width; + } + } + + yi = x; + stackIn = stackStart; + stackOut = stackEnd; + for ( y = 0; y < height; y++ ) + { + p = yi << 2; + pixels[p] = (r_sum * mul_sum) >> shg_sum; + pixels[p+1] = (g_sum * mul_sum) >> shg_sum; + pixels[p+2] = (b_sum * mul_sum) >> shg_sum; + + r_sum -= r_out_sum; + g_sum -= g_out_sum; + b_sum -= b_out_sum; + + r_out_sum -= stackIn.r; + g_out_sum -= stackIn.g; + b_out_sum -= stackIn.b; + + p = ( x + (( ( p = y + radiusPlus1) < heightMinus1 ? p : heightMinus1 ) * width )) << 2; + + r_sum += ( r_in_sum += ( stackIn.r = pixels[p])); + g_sum += ( g_in_sum += ( stackIn.g = pixels[p+1])); + b_sum += ( b_in_sum += ( stackIn.b = pixels[p+2])); + + stackIn = stackIn.next; + + r_out_sum += ( pr = stackOut.r ); + g_out_sum += ( pg = stackOut.g ); + b_out_sum += ( pb = stackOut.b ); + + r_in_sum -= pr; + g_in_sum -= pg; + b_in_sum -= pb; + + stackOut = stackOut.next; + + yi += width; + } + } + + context.putImageData( imageData, top_x, top_y ); + +} + +function BlurStack() +{ + this.r = 0; + this.g = 0; + this.b = 0; + this.a = 0; + this.next = null; +} \ No newline at end of file diff --git a/dammit/viewer/static/canvg.js b/dammit/viewer/static/canvg.js new file mode 100644 index 00000000..7e98e475 --- /dev/null +++ b/dammit/viewer/static/canvg.js @@ -0,0 +1,2841 @@ +/* + * canvg.js - Javascript SVG parser and renderer on Canvas + * MIT Licensed + * Gabe Lerner (gabelerner@gmail.com) + * http://code.google.com/p/canvg/ + * + * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + */ +(function(){ + // canvg(target, s) + // empty parameters: replace all 'svg' elements on page with 'canvas' elements + // target: canvas element or the id of a canvas element + // s: svg string, url to svg file, or xml document + // opts: optional hash of options + // ignoreMouse: true => ignore mouse events + // ignoreAnimation: true => ignore animations + // ignoreDimensions: true => does not try to resize canvas + // ignoreClear: true => does not clear canvas + // offsetX: int => draws at a x offset + // offsetY: int => draws at a y offset + // scaleWidth: int => scales horizontally to width + // scaleHeight: int => scales vertically to height + // renderCallback: function => will call the function after the first render is completed + // forceRedraw: function => will call the function on every frame, if it returns true, will redraw + this.canvg = function (target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + var svgTags = document.getElementsByTagName('svg'); + for (var i=0; i]*>/, ''); + var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + } + + svg.Property = function(name, value) { + this.name = name; + this.value = value; + } + svg.Property.prototype.getValue = function() { + return this.value; + } + + svg.Property.prototype.hasValue = function() { + return (this.value != null && this.value !== ''); + } + + // return the numerical value of the property + svg.Property.prototype.numValue = function() { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + svg.Property.prototype.valueOrDefault = function(def) { + if (this.hasValue()) return this.value; + return def; + } + + svg.Property.prototype.numValueOrDefault = function(def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + // color extensions + // augment the current color value with the opacity + svg.Property.prototype.addOpacity = function(opacity) { + var newValue = this.value; + if (opacity != null && opacity != '' && typeof(this.value)=='string') { // can only add opacity to colors, not patterns + var color = new RGBColor(this.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')'; + } + } + return new svg.Property(this.name, newValue); + } + + // definition extensions + // get the definition from the definitions table + svg.Property.prototype.getDefinition = function() { + var name = this.value.match(/#([^\)'"]+)/); + if (name) { name = name[1]; } + if (!name) { name = this.value; } + return svg.Definitions[name]; + } + + svg.Property.prototype.isUrlDefinition = function() { + return this.value.indexOf('url(') == 0 + } + + svg.Property.prototype.getFillStyleDefinition = function(e, opacityProp) { + var def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } + + // pattern + if (def != null && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + var pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } + } + return def.createPattern(svg.ctx, e); + } + + return null; + } + + // length extensions + svg.Property.prototype.getDPI = function(viewPort) { + return 96.0; // TODO: compute? + } + + svg.Property.prototype.getEM = function(viewPort) { + var em = 12; + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); + + return em; + } + + svg.Property.prototype.getUnits = function() { + var s = this.value+''; + return s.replace(/[0-9\.\-]/g,''); + } + + // get the length as pixels + svg.Property.prototype.toPixels = function(viewPort, processPercent) { + if (!this.hasValue()) return 0; + var s = this.value+''; + if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); + if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.match(/px$/)) return this.numValue(); + if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.match(/pc$/)) return this.numValue() * 15; + if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); + if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + var n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + } + + // time extensions + // get the time as milliseconds + svg.Property.prototype.toMilliseconds = function() { + if (!this.hasValue()) return 0; + var s = this.value+''; + if (s.match(/s$/)) return this.numValue() * 1000; + if (s.match(/ms$/)) return this.numValue(); + return this.numValue(); + } + + // angle extensions + // get the angle as radians + svg.Property.prototype.toRadians = function() { + if (!this.hasValue()) return 0; + var s = this.value+''; + if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + } + + // fonts + svg.Font = new (function() { + this.Styles = 'normal|italic|oblique|inherit'; + this.Variants = 'normal|small-caps|inherit'; + this.Weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit'; + + this.CreateFont = function(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } + } + } + + var that = this; + this.Parse = function(s) { + var f = {}; + var d = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false } + var ff = ''; + for (var i=0; i this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + this.addX = function(x) { this.addPoint(x, null); } + this.addY = function(y) { this.addPoint(null, y); } + + this.addBoundingBox = function(bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + + this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html + var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (i=0; i<=1; i++) { + var f = function(t) { + return Math.pow(1-t, 3) * p0[i] + + 3 * Math.pow(1-t, 2) * t * p1[i] + + 3 * (1-t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + } + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a == 0) { + if (b == 0) continue; + var t = -c / b; + if (0 < t && t < 1) { + if (i == 0) this.addX(f(t)); + if (i == 1) this.addY(f(t)); + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i == 0) this.addX(f(t1)); + if (i == 1) this.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i == 0) this.addX(f(t2)); + if (i == 1) this.addY(f(t2)); + } + } + } + + this.isPointInBox = function(x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + } + + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + // transforms + svg.Transform = function(v) { + var that = this; + this.Type = {} + + // translate + this.Type.translate = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + } + this.unapply = function(ctx) { + ctx.translate(-1.0 * this.p.x || 0.0, -1.0 * this.p.y || 0.0); + } + this.applyToPoint = function(p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + } + } + + // rotate + this.Type.rotate = function(s) { + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function(ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + } + this.unapply = function(ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(-1.0 * this.angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + } + this.applyToPoint = function(p) { + var a = this.angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + } + } + + this.Type.scale = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + } + this.unapply = function(ctx) { + ctx.scale(1.0 / this.p.x || 1.0, 1.0 / this.p.y || this.p.x || 1.0); + } + this.applyToPoint = function(p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + } + } + + this.Type.matrix = function(s) { + this.m = svg.ToNumberArray(s); + this.apply = function(ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + } + this.applyToPoint = function(p) { + p.applyTransform(this.m); + } + } + + this.Type.SkewBase = function(s) { + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + } + this.Type.SkewBase.prototype = new this.Type.matrix; + + this.Type.skewX = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; + } + this.Type.skewX.prototype = new this.Type.SkewBase; + + this.Type.skewY = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; + } + this.Type.skewY.prototype = new this.Type.SkewBase; + + this.transforms = []; + + this.apply = function(ctx) { + for (var i=0; i=0; i--) { + this.transforms[i].unapply(ctx); + } + } + + this.applyToPoint = function(p) { + for (var i=0; i= this.tokens.length - 1; + } + + this.isCommandOrEnd = function() { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + } + + this.isRelativeCommand = function() { + switch(this.command) + { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + break; + } + return false; + } + + this.getToken = function() { + this.i++; + return this.tokens[this.i]; + } + + this.getScalar = function() { + return parseFloat(this.getToken()); + } + + this.nextCommand = function() { + this.previousCommand = this.command; + this.command = this.getToken(); + } + + this.getPoint = function() { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + } + + this.getAsControlPoint = function() { + var p = this.getPoint(); + this.control = p; + return p; + } + + this.getAsCurrentPoint = function() { + var p = this.getPoint(); + this.current = p; + return p; + } + + this.getReflectedControlPoint = function() { + if (this.previousCommand.toLowerCase() != 'c' && + this.previousCommand.toLowerCase() != 's' && + this.previousCommand.toLowerCase() != 'q' && + this.previousCommand.toLowerCase() != 't' ){ + return this.current; + } + + // reflect point + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + } + + this.makeAbsolute = function(p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + return p; + } + + this.addMarker = function(p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length-1] == null) { + this.angles[this.angles.length-1] = this.points[this.points.length-1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + } + + this.addMarkerAngle = function(p, a) { + this.points.push(p); + this.angles.push(a); + } + + this.getMarkerPoints = function() { return this.points; } + this.getMarkerAngles = function() { + for (var i=0; i 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx,2)*Math.pow(ry,2))-(Math.pow(rx,2)*Math.pow(currp.y,2))-(Math.pow(ry,2)*Math.pow(currp.x,2))) / + (Math.pow(rx,2)*Math.pow(currp.y,2)+Math.pow(ry,2)*Math.pow(currp.x,2)) + ); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + var m = function(v) { return Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)); } + // ratio between two vectors + var r = function(u, v) { return (u[0]*v[0]+u[1]*v[1]) / (m(u)*m(v)) } + // angle between two vectors + var a = function(u, v) { return (u[0]*v[1] < u[1]*v[0] ? -1 : 1) * Math.acos(r(u,v)); } + // initial angle + var a1 = a([1,0], [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]); + // angle delta + var u = [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]; + var v = [(-currp.x-cpp.x)/rx,(-currp.y-cpp.y)/ry]; + var ad = a(u, v); + if (r(u,v) <= -1) ad = Math.PI; + if (r(u,v) >= 1) ad = 0; + + // for markers + var dir = 1 - sweepFlag ? 1.0 : -1.0; + var ah = a1 + dir * (ad / 2.0); + var halfWay = new svg.Point( + centp.x + rx * Math.cos(ah), + centp.y + ry * Math.sin(ah) + ); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + var r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1/sx, 1/sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + case 'z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + + this.getMarkers = function() { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + + var markers = []; + for (var i=0; i 1) this.offset = 1; + + var stopColor = this.style('stop-color'); + if (this.style('stop-opacity').hasValue()) stopColor = stopColor.addOpacity(this.style('stop-opacity').value); + this.color = stopColor.value; + } + svg.Element.stop.prototype = new svg.Element.ElementBase; + + // animation base element + svg.Element.AnimateBase = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + svg.Animations.push(this); + + this.duration = 0.0; + this.begin = this.attribute('begin').toMilliseconds(); + this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); + + this.getProperty = function() { + var attributeType = this.attribute('attributeType').value; + var attributeName = this.attribute('attributeName').value; + + if (attributeType == 'CSS') { + return this.parent.style(attributeName, true); + } + return this.parent.attribute(attributeName, true); + }; + + this.initialValue = null; + this.initialUnits = ''; + this.removed = false; + + this.calcValue = function() { + // OVERRIDE ME! + return ''; + } + + this.update = function(delta) { + // set initial value + if (this.initialValue == null) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); + } + + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value == 'indefinite' + || this.attribute('repeatDur').value == 'indefinite') { + this.duration = 0.0 + } + else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.initialValue; + return true; + } + else { + return false; // no updates made + } + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + this.from = this.attribute('from'); + this.to = this.attribute('to'); + this.values = this.attribute('values'); + if (this.values.hasValue()) this.values.value = this.values.value.split(';'); + + // fraction of duration we've covered + this.progress = function() { + var ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; + if (this.values.hasValue()) { + var p = ret.progress * (this.values.value.length - 1); + var lb = Math.floor(p), ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } + else { + ret.from = this.from; + ret.to = this.to; + } + return ret; + } + } + svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; + + // animate element + svg.Element.animate = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var p = this.progress(); + + // tween value linearly + var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + return newValue + this.initialUnits; + }; + } + svg.Element.animate.prototype = new svg.Element.AnimateBase; + + // animate color element + svg.Element.animateColor = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var p = this.progress(); + var from = new RGBColor(p.from.value); + var to = new RGBColor(p.to.value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * p.progress; + var g = from.g + (to.g - from.g) * p.progress; + var b = from.b + (to.b - from.b) * p.progress; + return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; + } + return this.attribute('from').value; + }; + } + svg.Element.animateColor.prototype = new svg.Element.AnimateBase; + + // animate transform element + svg.Element.animateTransform = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var p = this.progress(); + + // tween value linearly + var from = svg.ToNumberArray(p.from.value); + var to = svg.ToNumberArray(p.to.value); + var newValue = ''; + for (var i=0; i startI && child.attribute('x').hasValue()) break; // new group + width += child.measureTextRecursive(ctx); + } + return -1 * (textAnchor == 'end' ? width : width / 2.0); + } + return 0; + } + + this.renderChild = function(ctx, parent, i) { + var child = parent.children[i]; + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); + } + else { + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); + child.x = this.x; + } + this.x = child.x + child.measureText(ctx); + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + } + else { + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); + child.y = this.y; + } + this.y = child.y; + + child.render(ctx); + + for (var i=0; i0 && text[i-1]!=' ' && i0 && text[i-1]!=' ' && (i == text.length-1 || text[i+1]==' ')) arabicForm = 'initial'; + if (typeof(font.glyphs[c]) != 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type == 'glyph') glyph = font.glyphs[c]; + } + } + else { + glyph = font.glyphs[c]; + } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + } + + this.renderChildren = function(ctx) { + var customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + var text = this.getText(); + if (customFont.isRTL) text = text.split("").reverse().join(""); + + var dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (var i=0; i 0) { + var urlStart = srcs[s].indexOf('url'); + var urlEnd = srcs[s].indexOf(')', urlStart); + var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); + var doc = svg.parseXml(svg.ajax(url)); + var fonts = doc.getElementsByTagName('font'); + for (var f=0; fName: " + d.name + ""; + }); + + this.container.call(this.tip); + + // Draw the plots + for(var i=0; i < this.tracks.length; i++) { + + if('undefined' !== typeof this.tracks[i].visible) { + if(! this.tracks[i].visible) { + continue; + } + } else { + // If the user didn't set visible, + // then it's visible. + this.tracks[i].visible = true; + } + + // We're going to see what type of tracks we have + // and dispatch them appropriately + + switch(this.tracks[i].trackType) { + case "plot": + tracks[i].rad_per_elem = tracks[i].bp_per_element*this.layout.radians_pre_bp; + this.drawPlot(i); + break; + case "track": + this.drawTrack(i); + break; + case "stranded": + this.drawTrack(i); + break; + case "gap": + this.drawGap(i); + break; + case "glyph": + this.findGlyphTypes(i); + + if('undefined' !== typeof this.tracks[i].hideTypes) { + this.maskGlyphTypes(i, this.tracks[i].hideTypes) + } + // this.tracks[i].container = + // this.g.append("g") + // .attr("class", this.tracks[i].trackName + "_glyph_container") + this.drawGlyphTrack(i); + break; + default: + // Do nothing for an unknown track type + } + } + + // Resize dragger + if(this.layout.dragresize == true) { + var dragright = d3.behavior.drag() + .on("dragstart", this.dragresize_start.bind(this)) + .on("drag", this.dragresize.bind(this)) + .on("dragend", this.dragresize_end.bind(this)); + + this.dragbar_y_mid = this.layout.h/2; + this.dragbar = this.container.append("g") + .attr("transform", "translate(" + (this.layout.w+this.layout.ExtraWidthX-25) + "," + (this.layout.h+this.layout.ExtraWidthY-25) + ")") + .attr("width", 25) + .attr("height", 20) + .attr("fill", "lightblue") + .attr("fill-opacity", .2) + .attr("cursor", "ew-resize") + .call(dragright); + + this.dragbar.append("rect") + .attr("width", 25) + .attr("height", 20) + .attr("fill-opacity", 0); + + this.dragbar.append("line") + .attr("x1", 16) + .attr("x2", 16) + .attr("y1", 0) + .attr("y2", 14) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 2) + .attr("x2", 16) + .attr("y1", 14) + .attr("y2", 14) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 19) + .attr("x2", 19) + .attr("y1", 0) + .attr("y2", 17) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 2) + .attr("x2", 19) + .attr("y1", 17) + .attr("y2", 17) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 22) + .attr("x2", 22) + .attr("y1", 0) + .attr("y2", 20) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 2) + .attr("x2", 22) + .attr("y1", 20) + .attr("y2", 20) + .attr("class", "dragbar-line"); + } + +} + +circularTrack.prototype.drawAxis = function() { + var cfg = this.layout; + var g = this.g; + + this.axis_container = this.g + .append("g") + .attr("id", "axis_container"); + + var axis = this.axis_container.selectAll(".axis") + .data(d3.range(0,cfg.genomesize, cfg.spacing)) + .enter() + .append("g") + .attr("class", "axis"); + + axis.append("line") + .attr("x1", function(d, i){return cfg.w2 + (20*Math.cos((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("y1", function(d, i){return cfg.h2 + (20*Math.sin((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("x2", function(d, i){return cfg.w2 + (cfg.radius*Math.cos((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("y2", function(d, i){return cfg.h2 + (cfg.radius*Math.sin((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("class", "line") + .style("stroke", "grey") + .style("stroke-width", "1px"); + + var axis_label = this.axis_container.selectAll(".axislabel") + .data(d3.range(0,cfg.genomesize, cfg.spacing*cfg.legend_spacing)) + .enter() + .append("g") + .attr("class", "axislabel"); + + axis_label.append("text") + .attr("class", "legend") + .text(function(d){ var prefix = d3.formatPrefix(d); + return prefix.scale(d) + prefix.symbol; + }) + .style("font-family", "sans-serif") + .style("font-size", "11px") + .attr("text-anchor", "middle") + .attr("dy", "1.5em") + .attr("transform", function(d, i){return "translate(0, -10)"}) + .attr("x", function(d, i){return cfg.w2 + ((cfg.radius+10)*Math.cos((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("y", function(d, i){return cfg.h2 + ((cfg.radius+10)*Math.sin((d*cfg.radians_pre_bp)-cfg.PI2));}); + + // And draw the pretty outer circle for the axis + this.drawCircle("outerAxis", cfg.radius-10, 'grey'); +} + +circularTrack.prototype.moveAxis = function() { + var cfg = this.layout; + + this.axis_container + .selectAll("line") + .data(d3.range(0,cfg.genomesize, cfg.spacing)) + .transition() + .duration(1000) + .attr("x1", function(d, i){return cfg.w2 + (20*Math.cos((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("y1", function(d, i){return cfg.h2 + (20*Math.sin((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("x2", function(d, i){return cfg.w2 + (cfg.radius*Math.cos((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("y2", function(d, i){return cfg.h2 + (cfg.radius*Math.sin((d*cfg.radians_pre_bp)-cfg.PI2));}); + + this.axis_container + .selectAll("text") + .data(d3.range(0,cfg.genomesize, cfg.spacing*cfg.legend_spacing)) + .transition() + .duration(1000) + .attr("x", function(d, i){return cfg.w2 + ((cfg.radius+10)*Math.cos((d*cfg.radians_pre_bp)-cfg.PI2));}) + .attr("y", function(d, i){return cfg.h2 + ((cfg.radius+10)*Math.sin((d*cfg.radians_pre_bp)-cfg.PI2));}); + + // And draw the pretty outer circle for the axis + this.moveCircle("outerAxis", cfg.radius-10); + +} +// Helper function for drawing needed circles such +// as in stranded tracks +// Can be called standalone in setting up the look +// of your genome + +circularTrack.prototype.drawCircle = function(name, radius, line_stroke, animate) { + var g = this.g; + var cfg = this.layout; + + g.append("circle") + .attr("r", (('undefined' == typeof animate) ? radius : 1 )) + .attr("class", name + "_circle") + .style("fill", "none") + .style("stroke", line_stroke) + .attr("cx", cfg.w2) + .attr("cy", cfg.h2); + + // An animated entrance + if('undefined' !== typeof animate) { + this.moveCircle(name, radius); + } + +} + +// Change the radius of an inscribed circle + +circularTrack.prototype.moveCircle = function(name, radius) { + var g = this.g; + var cfg = this.layout; + + g.selectAll("." + name + "_circle") + .transition() + .duration(1000) + .attr("r", radius) + .attr("cx", cfg.w2) + .attr("cy", cfg.h2); + +} + +// Remove a drawn circle, in a pretty animated way + +circularTrack.prototype.removeCircle = function(name) { + var g = this.g; + + g.selectAll("." + name + "_circle") + .transition() + .duration(1000) + .attr("r", 1) + .style("opacity", 0) + .remove(); +} + +///////////////////////////////////////// +// +// Plot type tracks (as in line graphs) +// +///////////////////////////////////////// + +circularTrack.prototype.drawPlot = function(i, animate) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + + this.tracks[i].plotRange = d3.scale.linear() + .domain([track.plot_min, track.plot_max]) + .range([track.plot_radius-(track.plot_width/2), track.plot_radius+(track.plot_width/2)]); + + var plotRange = this.tracks[i].plotRange; + + var lineFunction = d3.svg.line() + .x(function(d, i) { return cfg.w2 + ((('undefined' == typeof animate) ? plotRange(d) : 1 )*Math.cos((i*track.rad_per_elem)-(cfg.PI2))); }) + .y(function(d, i) { return cfg.h2 + ((('undefined' == typeof animate) ? plotRange(d) : 1 )*Math.sin((i*track.rad_per_elem)-(cfg.PI2))); }) + .interpolate("linear"); + + g.append("path") + .attr("d", lineFunction(track.items)) + .attr("class", track.trackName) + .attr("id", track.trackName) + .attr("stroke-width", 1) + .attr("fill", "none"); + + // Now do the mean circle if we have one + if('undefined' !== typeof track.plot_mean) { + this.drawCircle(track.trackName, this.tracks[i].plotRange(track.plot_mean), "grey", animate); + } + + // And if we're doing an animated entrance... + if('undefined' !== typeof animate) { + this.movePlot(i, track.plot_radius); + } + + // Mark the track as visible, if not already + this.tracks[i].visible = true; +} + +circularTrack.prototype.movePlot = function(i, radius) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + + // Save this in case this is a change of radius + // ratherthan an animated entrance + if('undefined' !== typeof this.tracks[i].plot_radius) { + this.tracks[i].plot_radius = radius; + } + + // We needed to save the new radius but if this + // track isn't visible, do nothing + if(! this.tracks[i].visible) { + return; + } + + this.tracks[i].plotRange + .range([track.plot_radius-(track.plot_width/2), track.plot_radius+(track.plot_width/2)]); + + var plotRange = this.tracks[i].plotRange; + + var lineFunction = d3.svg.line() + .x(function(d, i, j) { return cfg.w2 + (plotRange(d)*Math.cos((i*track.rad_per_elem)-(cfg.PI2))); }) + .y(function(d, i) { return cfg.h2 + (plotRange(d)*Math.sin((i*track.rad_per_elem)-(cfg.PI2))); }) + .interpolate("linear"); + + var plot = g.selectAll("." + track.trackName) + + plot.transition() + .duration(1000) + .attr("d", function(d,i) { return lineFunction(track.items)}); + + // Now move the mean circle if we have one + if('undefined' !== typeof track.plot_mean) { + this.moveCircle(track.trackName, this.tracks[i].plotRange(track.plot_mean)); + } +} + +circularTrack.prototype.removePlot = function(i) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + + var plotRange = d3.scale.linear() + .domain([track.plot_min, track.plot_max]) + .range([1-(track.plot_width/2), 1+(track.plot_width/2)]); + + var lineFunction = d3.svg.line() + .x(function(d, i) { return cfg.w2 + (plotRange(d)*Math.cos((i*track.rad_per_elem)-(cfg.PI2))); }) + .y(function(d, i) { return cfg.h2 + (plotRange(d)*Math.sin((i*track.rad_per_elem)-(cfg.PI2))); }) + .interpolate("linear"); + + g.selectAll("." + track.trackName) + .transition() + .duration(1000) + .attr("d", lineFunction(track.items)) + .style("opacity", 0) + .remove(); + + if('undefined' !== typeof track.plot_mean) { + this.removeCircle(track.trackName); + } + + // Mark the track as not visible + this.tracks[i].visible = false; +} + + +//////////////////////////////////////////////// +// +// Track type tracks (as blocks without strands) +// +//////////////////////////////////////////////// + +circularTrack.prototype.drawTrack = function(i, animate) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + + // Because of how the tooltip library binds to the SVG object we have to turn it + // on or off here rather than in the .on() call, we'll redirect the calls to + // a dummy do-nothing object if we're not showing tips in this context. + var tip = {show: function() {}, hide: function() {} }; + if(('undefined' !== typeof track.showTooltip) && track.showTooltip) { + tip = this.tip; + } + + // The arc object which will be passed in to each + // set of data + var arc = d3.svg.arc() + .innerRadius(function(d){ return (('undefined' == typeof animate) ? + calcInnerRadius(track.inner_radius, track.outer_radius, d.strand) + : 1);}) + .outerRadius(function(d){ return (('undefined' == typeof animate) ? + calcOuterRadius(track.inner_radius, track.outer_radius, d.strand) + : 2);}) + .startAngle(function(d){if(track.min_slice && (d.end - d.start) < cfg.min_bp_per_slice) { + return (d.start - ((d.end - d.start - cfg.min_bp_per_slice_half) / 2))*cfg.radians_pre_bp; + } else { + return cfg.radians_pre_bp*d.start; + } + }) + .endAngle(function(d){if(track.min_slice && (d.end - d.start) < cfg.min_bp_per_slice) { + return (d.end + ((d.end - d.start - cfg.min_bp_per_slice_half)/2))*cfg.radians_pre_bp; + } else { + return cfg.radians_pre_bp*d.end; + } + }); + + // Draw the track, putting in elements such as hover colour change + // if one exists, click events, etc + g.selectAll(".tracks."+track.trackName) + .data(track.items) + .enter() + .append("path") + .attr("d", arc) + .attr("class", function(d) { return track.trackName + ('undefined' !== typeof d.strand ? '_' + (d.strand == 1 ? 'pos' : 'neg') : '') + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : '') }) + .attr("transform", "translate("+cfg.w2+","+cfg.h2+")") + .on("click", function(d,i) { + if('undefined' !== typeof track.mouseclick) { + var fn = window[track.mouseclick]; + if('object' == typeof fn) { +// console.log(fn); + return fn.onclick(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + + } else { + null; + } + }) + .on("mouseover", function(d, i) { + tip.show(d); + if('undefined' !== typeof track.mouseover_callback) { + var fn = window[track.mouseover_callback]; + if('object' == typeof fn) { + fn.mouseover(track.trackName, d, cfg.plotid); + return true; + } else if('function' == typeof fn) { + return fn(track.trackNamed, cfg.plotid); + } + + } else { + return null; + } + }) + .on("mouseout", function(d, i) { + tip.hide(d); + if('undefined' !== typeof track.mouseout_callback) { + var fn = window[track.mouseout_callback]; + if('object' == typeof fn) { + return fn.mouseout(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackNamed, cfg.plotid); + } + + } else { + return null; + } + }); + + // If we're doing an animated addition, move the track out to its + // new spot + if('undefined' !== typeof animate) { + this.moveTrack(i, track.inner_radius, track.outer_radius); + } + + // And check if we've been asked to do a centre line + if('undefined' !== typeof track.centre_line_stroke) { + this.drawCircle(track.trackName, (track.inner_radius + track.outer_radius)/2, track.centre_line_stroke, animate); + } + + this.tracks[i].visible = true; + +} + +circularTrack.prototype.moveTrack = function(i, innerRadius, outerRadius) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + + // Just record the new radii in case we need them later + if('undefined' !== typeof this.tracks[i].inner_radius) { + this.tracks[i].inner_radius = innerRadius; + } + if('undefined' !== typeof this.tracks[i].outer_radius) { + this.tracks[i].outer_radius = outerRadius; + } + + // We needed to save the new radius but if this + // track isn't visible, do nothing + if(! this.tracks[i].visible) { + return; + } + + var arcShrink = d3.svg.arc() + .innerRadius(function(d){return calcInnerRadius(innerRadius, outerRadius, d.strand);}) + .outerRadius(function(d){return calcOuterRadius(innerRadius, outerRadius, d.strand);}) + .startAngle(function(d){if(track.min_slice && (d.end - d.start) < cfg.min_bp_per_slice) { + return (d.start - ((d.end - d.start - cfg.min_bp_per_slice_half) / 2))*cfg.radians_pre_bp; + } else { + return cfg.radians_pre_bp*d.start; + } + }) + .endAngle(function(d){if(track.min_slice && (d.end - d.start) < cfg.min_bp_per_slice) { + return (d.end + ((d.end - d.start - cfg.min_bp_per_slice_half)/2))*cfg.radians_pre_bp; + } else { + return cfg.radians_pre_bp*d.end; + } + }); + + + // .endAngle(function(d){return cfg.radians_pre_bp*d.start;}) + // .startAngle(function(d){return cfg.radians_pre_bp*d.end;}); + + g.selectAll("." + track.trackName + ", ." + track.trackName + "_pos, ." + track.trackName + "_neg") + .transition() + .duration(1000) + .attr("d", arcShrink) + .attr("transform", "translate("+cfg.w2+","+cfg.h2+")") + + // And check if we've been asked to do a centre line + if('undefined' !== typeof track.centre_line_stroke) { + this.moveCircle(track.trackName, (track.inner_radius + track.outer_radius)/2); + } + +} + +circularTrack.prototype.removeTrack = function(i) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + + var arcShrink = d3.svg.arc() + .innerRadius(1) + .outerRadius(2) + .endAngle(function(d){return cfg.radians_pre_bp*d.start;}) + .startAngle(function(d){return cfg.radians_pre_bp*d.end;}); + + g.selectAll("." + track.trackName + ", ." + track.trackName + "_pos, ." + track.trackName + "_neg") + .transition() + .duration(1000) + .attr("d", arcShrink) + .style("opacity", 0) + .remove(); + + if('undefined' !== track.centre_line_stroke) { + this.removeCircle(track.trackName); + } + + this.tracks[i].visible = false; + +} + +//////////////////////////////////////////////// +// +// Gap type tracks +// +//////////////////////////////////////////////// + +circularTrack.prototype.drawGap = function(i, animate) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + var self = this; + + var gap_range = ('undefined' == typeof animate) ? d3.range(track.inner_radius, track.outer_radius, 8) : [1,2]; + + // Draw the track, putting in elements such as hover colour change + // if one exists, click events, etc + var gaps = g.selectAll(".tracks."+track.trackName) + .data(track.items) + .enter() + .append("g") + .attr("class", function(d) { return track.trackName + ' ' + track.trackName + '_g ' + track.trackName + '_' + d.name + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : '') }) + .attr("transform", "translate("+cfg.w2+","+cfg.h2+")") + .each(function(d) { + d3.select(this) + .append("path") + .attr("d", self.jaggedLineGenerator(d.start, gap_range)) + .attr("class", function(d2) { return track.trackName + ' ' + track.trackName + '_line' + ('undefined' !== typeof d.extraclass ? d.extraclass : '') }) + .attr("stroke-width", 1) + .attr("fill", "none"); + + d3.select(this) + .append("path") + .attr("d", self.jaggedLineGenerator(d.end, gap_range)) + .attr("class", function(d2) { return track.trackName + ' ' + track.trackName + '_line' + ('undefined' !== typeof d.extraclass ? d.extraclass : '') }) + .attr("stroke-width", 1) + .attr("fill", "none"); + }); + + // If we're doing an animated addition, move the track out to its + // new spot + if('undefined' !== typeof animate) { + this.moveGap(i, track.inner_radius, track.outer_radius); + } + +} + +circularTrack.prototype.moveGap = function(i, innerRadius, outerRadius) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + var self = this; + + // Just record the new radii in case we need them later + if('undefined' !== typeof this.tracks[i].inner_radius) { + this.tracks[i].inner_radius = innerRadius; + } + if('undefined' !== typeof this.tracks[i].outer_radius) { + this.tracks[i].outer_radius = outerRadius; + } + + // We needed to save the new radius but if this + // track isn't visible, do nothing + if(! this.tracks[i].visible) { + return; + } + + var gap_range = d3.range(innerRadius, outerRadius, 8); + + g.selectAll("." + track.trackName + '_g') + .transition() + .duration(1000) + .attr("transform", "translate("+cfg.w2+","+cfg.h2+")") + + g.selectAll('.' + track.trackName + '_line') + .transition() + .duration(1000) + .attrTween("d", function(d) { return pathTween(this, self.jaggedLineGenerator(d.start, gap_range), 4) }); + + +} + +circularTrack.prototype.removeGap = function(i) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + var self = this; + + g.selectAll('.' + track.trackName + '_line') + .transition() + .duration(1000) + .attrTween("d", function(d) { return pathTween(this, self.jaggedLineGenerator(d.start, [1,2]), 4) }) + .remove(); + + g.selectAll("." + track.trackName + '_g') + .transition() + .duration(1000) + .style('opacity', 0) + .remove() + + this.tracks[i].visible = false; + +} + +// Jagged line path generator for the gap track type + +circularTrack.prototype.jaggedLineGenerator = function(bp, data) { + var cfg = this.layout; + + var generator = d3.svg.line() + .x(function(d, i) { var offset = ((i % 2 === 0) ? 0.02 : -0.02); return d * Math.cos((bp*cfg.radians_pre_bp)-cfg.PI2+(i ? offset : 0) ); } ) + .y(function(d, i) { var offset = ((i % 2 === 0) ? 0.02 : -0.02); return d * Math.sin((bp*cfg.radians_pre_bp)-cfg.PI2+ (i ? offset : 0) ); } ) + .interpolate("linear"); + + return generator(data); +} + +function pathTween(path, d1, precision) { + // return function() { + // var path0 = this, + var path0 = path, + path1 = path0.cloneNode(), + n0 = path0.getTotalLength(), + n1 = (path1.setAttribute("d", d1), path1).getTotalLength(); + + // Uniform sampling of distance based on specified precision. + var distances = [0], i = 0, dt = precision / Math.max(n0, n1); + while ((i += dt) < 1) distances.push(i); + distances.push(1); + + // Compute point-interpolators at each distance. + var points = distances.map(function(t) { + var p0 = path0.getPointAtLength(t * n0), + p1 = path1.getPointAtLength(t * n1); + return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]); + }); + + return function(t) { + return t < 1 ? "M" + points.map(function(p) { return p(t); }).join("L") : d1; + }; + // }; +} + +//////////////////////////////////////////////// +// +// Glyph type tracks +// +//////////////////////////////////////////////// + +// We will probably need to send a localized +// version of the data so the update works +// properly + +circularTrack.prototype.drawGlyphTrack = function(i) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + var stack_count = 0; + + var items = track.items.filter(function(d) { return track.visTypes.contains(d.type) } ); + + // Because on update the order of processing changes we need + // to recompute the stacking order manually each time + for(var i = 0; i < items.length; i++) { + if(i < 1) { + items[i].stackCount = 0; + continue; + } + + xs = (cfg.h/2 + (track.radius*Math.sin((items[i].bp*cfg.radians_pre_bp)-cfg.PI2))) - + (cfg.h/2 + (track.radius*Math.sin((items[i-1].bp*cfg.radians_pre_bp)-cfg.PI2))); + ys = (cfg.h/2 + (track.radius*Math.cos((items[i].bp*cfg.radians_pre_bp)-cfg.PI2))) - + (cfg.h/2 + (track.radius*Math.cos((items[i-1].bp*cfg.radians_pre_bp)-cfg.PI2))); + xs = xs * xs; + ys = ys * ys; + var dist = Math.sqrt(xs + ys); + + if(dist < track.pixel_spacing) { + items[i].stackCount = items[i-1].stackCount + 1; + continue; + } + + items[i].stackCount = 0; + } + + var x = function(d,i) { return cfg.w2 + (((track.glyph_buffer * d.stackCount) + track.radius)*Math.cos((d.bp*cfg.radians_pre_bp)-cfg.PI2)); }; + var y = function(d,i) { return cfg.h2 + (((track.glyph_buffer * d.stackCount) + track.radius)*Math.sin((d.bp*cfg.radians_pre_bp)-cfg.PI2)); }; + + var trackPath = g.selectAll("." + track.trackName) + // var trackPath = track.container.selectAll("." + track.trackName) + .data(items, function(d) { return d.id; }); + + trackPath.transition() + .duration(1000) + .attr("transform", function(d,i) { return "translate(" + x(d,i) + "," + + y(d,i) + ")" }) + .style("opacity", 1); + + trackPath.exit() + .transition() + .duration(1000) + .attr("transform", "translate(" + cfg.h2 + "," + cfg.w2 + ")") + .style("opacity", 0) + .remove(); + + trackPath.enter() + .append('path') + .attr('id', function(d,i) { return track.trackName + "_glyph" + d.id; }) + .attr('class', function(d) {return track.trackName + '_' + d.type + ' ' + track.trackName}) + .attr("d", d3.svg.symbol().type(track.glyphType).size(track.glyphSize)) + .attr("transform", "translate(" + cfg.h2 + "," + cfg.w2 + ")") + .style("opacity", 0) + .transition() + .duration(1000) + .attr("transform", function(d,i) { return "translate(" + x(d,i) + "," + + y(d,i) + ")" }) + .style("opacity", 1); + + +} + +circularTrack.prototype.updateGlyphTrack = function(i) { + var g = this.g; + var cfg = this.layout; + var track = this.tracks[i]; + var stack_count = 0; + + +} + +//////////////////////////////////////////////// +// +// Brush functionality +// +//////////////////////////////////////////////// + +circularTrack.prototype.attachBrush = function(callbackObj) { + if('undefined' !== typeof this.callbackObj) { + + if( Object.prototype.toString.call( callbackObj ) === '[object Array]' ) { + this.callbackObj.push(callbackObj); + } else { + var tmpobj = this.callbackObj; + this.callbackObj = [tmpobj, callbackObj]; + } + } else { + this.callbackObj = callbackObj; + this.createBrush(); + } + +} + +circularTrack.prototype.redrawBrush = function(startRad, endRad) { + var cfg = this.layout; + + if('undefined' !== typeof this.brush) { + + this.brushArc + .outerRadius(cfg.radius-10); + + this.brush + .transition() + .duration(1000) + .attr("transform", "translate("+cfg.w2+","+cfg.h2+")"); + + this.moveBrush(startRad, endRad); + + d3.select("#brushStart_" + cfg.containerid) + .transition() + .duration(1000) + .attr("cx", cfg.h/2 + ((cfg.radius-10)*Math.cos(startRad-cfg.PI2))) + .attr("cy", cfg.h/2 + ((cfg.radius-10)*Math.sin(startRad-cfg.PI2))); + + d3.select("#brushEnd_" + cfg.containerid) + .transition() + .duration(1000) + .attr("cx", cfg.w2 + ((cfg.radius-10)*Math.cos(endRad-cfg.PI2))) + .attr("cy", cfg.h2 + ((cfg.radius-10)*Math.sin(endRad-cfg.PI2))); + + } +} + +circularTrack.prototype.createBrush = function() { + var g = this.g; + var cfg = this.layout; + var xScale = this.xScale; + var self = this; + + this.brushStart = 0; + this.brushEnd = 0; + this.brushStartBP = 0; + this.brushEndBP = 0; + + this.brushArc = d3.svg.arc() + .innerRadius(20) + .outerRadius(cfg.radius-10) + .endAngle(function(d){return xScale(0);}) + .startAngle(function(d){return xScale(0);}); + + this.brush = g.insert("path", "defs") + .attr("d", this.brushArc) + .attr("id", "polarbrush_" + cfg.containerid) + .attr("class", "polarbrush circularbrush") + .attr("transform", "translate("+cfg.w2+","+cfg.h2+")") + + var dragStart = d3.behavior.drag() + .on("drag", function(d) { + var mx = d3.mouse(this)[0]; + var my = d3.mouse(this)[1]; + + var curRadandBP = calcRadBPfromXY((d3.mouse(this)[0] - (cfg.w2)), + -(d3.mouse(this)[1] - (cfg.h2)), + xScale); + + // Don't allow the brush to go beyond the other + if(curRadandBP[0] >= self.brushEnd) { + return; + } + + g.select("#brushStart_" + cfg.containerid) + .attr("cx", function(d, i){return cfg.h/2 + (cfg.radius-10)*Math.cos((curRadandBP[0])-cfg.PI2);}) + .attr("cy", function(d, i){return cfg.h/2 + (cfg.radius-10)*Math.sin((curRadandBP[0])-cfg.PI2); }); + + self.brushStart = curRadandBP[0]; + self.brushStartBP = curRadandBP[1]; + self.moveBrush(self.brushStart, self.brushEnd); + if('undefined' !== typeof self.callbackObj) { + self.doBrushCallback(self.brushStartBP, self.brushEndBP); + // self.callbackObj.update(self.brushStartBP, self.brushEndBP); + } + }) + .on("dragend", function(d) { + self.doBrushFinishedCallback(self.brushStartBP, self.brushEndBP); + }); + + var dragEnd = d3.behavior.drag() + .on("drag", function(d) { + var mx = d3.mouse(this)[0]; + var my = d3.mouse(this)[1]; + + var curRadandBP = calcRadBPfromXY((d3.mouse(this)[0] - (cfg.w2)), + -(d3.mouse(this)[1] - (cfg.h2)), + xScale); + + // Don't allow the brush to go beyond the other + if(curRadandBP[0] <= self.brushStart) { + return; + } + + g.select("#brushEnd_" + cfg.containerid) + .attr("cx", function(d, i){return cfg.h/2 + (cfg.radius-10)*Math.cos((curRadandBP[0])-cfg.PI2);}) + .attr("cy", function(d, i){return cfg.h/2 + (cfg.radius-10)*Math.sin((curRadandBP[0])-cfg.PI2); }); + + self.brushEnd = curRadandBP[0]; + self.brushEndBP = curRadandBP[1]; + self.moveBrush(self.brushStart, self.brushEnd); + if('undefined' !== typeof self.callbackObj) { + self.doBrushCallback(self.brushStartBP, self.brushEndBP); + // self.callbackObj.update(self.brushStartBP, self.brushEndBP); + } + }) + .on("dragend", function(d) { + self.doBrushFinishedCallback(self.brushStartBP, self.brushEndBP); + }); + + this.endBrushObj = g.append("circle") + .attr({ + id: 'brushEnd_' + cfg.containerid, + class: 'brushEnd circularbrush', + cx: (cfg.w2 + ((cfg.radius-10)*Math.cos((this.xScale(0))-cfg.PI2))), + cy: (cfg.h2 + ((cfg.radius-10)*Math.sin((this.xScale(0))-cfg.PI2))), + r: 5 + }) + .call(dragEnd); + + this.startBrushObj = g.append("circle") + .attr({ + id: 'brushStart_' + cfg.containerid, + class: 'brushStart circularbrush', + cx: (cfg.w2 + ((cfg.radius-10)*Math.cos((this.xScale(0))-cfg.PI2))), + cy: (cfg.h2 + ((cfg.radius-10)*Math.sin((this.xScale(0))-cfg.PI2))), + r: 5 + }) + .call(dragStart); + + // Create the start and stop pointers + +} + +circularTrack.prototype.doBrushCallback = function(startBP, endBP) { + var cfg = this.layout; + + if( Object.prototype.toString.call( this.callbackObj ) === '[object Array]' ) { + for(var obj in this.callbackObj) { + if(this.callbackObj.hasOwnProperty(obj)) { + this.callbackObj[obj].update(startBP, endBP, { plotid: cfg.plotid } ); + } + } + } else { + this.callbackObj.update(startBP, endBP, { plotid: cfg.plotid }); + } + +} + +circularTrack.prototype.doBrushFinishedCallback = function(startBP, endBP) { + var cfg = this.layout; + + if( Object.prototype.toString.call( this.callbackObj ) === '[object Array]' ) { + for(var obj in this.callbackObj) { + if(this.callbackObj.hasOwnProperty(obj)) { + this.callbackObj[obj].update_finished(startBP, endBP, { plotid: cfg.plotid }); + } + } + } else { + this.callbackObj.update_finished(startBP, endBP, { plotid: cfg.plotid }); + } + +} + +circularTrack.prototype.moveBrush = function(startRad, endRad) { + var g = this.g; + var cfg = this.layout; + + // console.log("moving brush to " + startRad, endRad); + + this.brushArc + .startAngle(startRad) + .endAngle(endRad); + + d3.select('#polarbrush_' + cfg.containerid) + .attr("d", this.brushArc); + + this.currentStart = startRad; + this.currentEnd = endRad; + +} + +circularTrack.prototype.moveBrushbyBP = function(startbp, endbp) { + var cfg = this.layout; + + var startRad = startbp*this.layout.radians_pre_bp; + var endRad = endbp*this.layout.radians_pre_bp; + this.moveBrush(startRad,endRad); + + this.brushStart = startRad; + this.brushStartBP = startbp; + this.currentStart = startRad; + this.currentEnd = endRad; + d3.select("#brushStart_" + cfg.containerid) + .attr("cx", cfg.h/2 + ((cfg.radius-10)*Math.cos(startRad-cfg.PI2))) + .attr("cy", cfg.h/2 + ((cfg.radius-10)*Math.sin(startRad-cfg.PI2))); + + this.brushEnd = endRad; + this.brushEndBP = endbp; + d3.select("#brushEnd_" + cfg.containerid) + .attr("cx", cfg.w2 + ((cfg.radius-10)*Math.cos(endRad-cfg.PI2))) + .attr("cy", cfg.h2 + ((cfg.radius-10)*Math.sin(endRad-cfg.PI2))); + + +} + +circularTrack.prototype.hideBrush = function() { + var cfg = this.layout; + + d3.select("#brushStart_" + cfg.containerid) + .style("visibility", "hidden"); + + d3.select("#brushEnd_" + cfg.containerid) + .style("visibility", "hidden"); + + d3.select('#polarbrush_' + cfg.containerid) + .style("visibility", "hidden"); +} + +circularTrack.prototype.showBrush = function() { + var cfg = this.layout; + + d3.select("#brushStart_" + cfg.containerid) + .style("visibility", "visible"); + + d3.select("#brushEnd_" + cfg.containerid) + .style("visibility", "visible"); + + d3.select('#polarbrush_' + cfg.containerid) + .style("visibility", "visible"); +} + + circularTrack.prototype.update = function(startBP, endBP, params) { + this.moveBrushbyBP(startBP, endBP); +} + + circularTrack.prototype.update_finished = function(startBP, endBP, params) { + // console.log("Thank you, got: " + startBP, endBP); +} + +//////////////////////////////////////////////// +// +// Export functionality +// +//////////////////////////////////////////////// + +// Allowed export formats are 'png' and 'svg' +// The extension will be added, just summply the base +// filename. + +// Saving to raster format is dependent on FileSaver.js +// and canvg.js (which include rgbcolor.js & StackBlur.js), +// they must be loaded before circularplot.js + +circularTrack.prototype.savePlot = function(scaling, filename, stylesheetfile, format) { + + // First lets get the stylesheet + var sheetlength = stylesheetfile.length; + var style = document.createElementNS("http://www.w3.org/1999/xhtml", "style"); + style.textContent += ""; + + // Now we clone the SVG element, resize and scale it up + var container = this.layout.container.slice(1); + var containertag = document.getElementById(container); + var clonedSVG = containertag.cloneNode(true); + var svg = clonedSVG.getElementsByTagName("svg")[0]; + + // Remove drag-resize shadow element + var tags = svg.getElementsByClassName("dragbar-shadow") + for(var i=0; i=0; i--) { +// if(tags[i].getAttributeNS(null, "name") === name) { + tags[i].parentNode.removeChild(tags[i]); +// } + } + + // Remove the move croshairs if on the chart + var tags = svg.getElementsByClassName("move_circularchart") + for(var i=tags.length-1; i>=0; i--) { +// if(tags[i].getAttributeNS(null, "name") === name) { + tags[i].parentNode.removeChild(tags[i]); +// } + } + + // We need to resize the svg with the new canvas size + svg.removeAttribute('width'); + svg.removeAttribute('height'); + svg.setAttribute('width', this.layout.w*scaling + this.layout.TranslateX*scaling); + svg.setAttribute('height', this.layout.h*scaling + this.layout.TranslateY*scaling); + + // Update first g tag with the scaling + g = svg.getElementsByTagName("g")[0]; + transform = g.getAttribute("transform"); + g.setAttribute("transform", transform + " scale(" + scaling + ")"); + + // Append the stylehsheet to the cloned svg element + // so when we export it the style are inline and + // get rendered + svg.getElementsByTagName("defs")[0].appendChild(style); + + // Fetch the actual SVG tag and convert it to a canvas + // element + var content = clonedSVG.innerHTML.trim(); + + if(format == 'svg') { + var a = document.createElement('a'); + a.href = "data:application/octet-stream;base64;attachment," + btoa(content); + a.download = filename + ".svg"; + a.click(); + + } else if(format == 'png') { + var canvas = document.createElement('canvas'); + canvg(canvas, content); + + // Convert the canvas to a data url (this could + // be displayed inline by inserting it in to an + // tag in the src attribute, ie + // + var theImage = canvas.toDataURL('image/png'); + + // Convert to a blob + var blob = this.dataURLtoBlob(theImage); + + // Prompt to save + saveAs(blob, filename); + } +} + +// Saving to raster format is dependent on FileSaver.js +// and canvg.js (which include rgbcolor.js & StackBlur.js), +// they must be loaded before circularplot.js + +circularTrack.prototype.saveRaster = function(scaling, filename, stylesheetfile) { + // First lets get the stylesheet + var sheetlength = stylesheetfile.length; + var style = document.createElementNS("http://www.w3.org/1999/xhtml", "style"); + style.textContent += ""; + + // Now we clone the SVG element, resize and scale it up + var container = this.layout.container.slice(1); + var containertag = document.getElementById(container); + var clonedSVG = containertag.cloneNode(true); + var svg = clonedSVG.getElementsByTagName("svg")[0]; + + // We need to resize the svg with the new canvas size + svg.removeAttribute('width'); + svg.removeAttribute('height'); + svg.setAttribute('width', this.layout.w*scaling + this.layout.TranslateX*scaling); + svg.setAttribute('height', this.layout.h*scaling + this.layout.TranslateY*scaling); + + // Update first g tag with the scaling + g = svg.getElementsByTagName("g")[0]; + transform = g.getAttribute("transform"); + g.setAttribute("transform", transform + " scale(" + scaling + ")"); + + // Append the stylehsheet to the cloned svg element + // so when we export it the style are inline and + // get rendered + svg.getElementsByTagName("defs")[0].appendChild(style); + + // Fetch the actual SVG tag and convert it to a canvas + // element + var content = clonedSVG.innerHTML.trim(); + var canvas = document.createElement('canvas'); + canvg(canvas, content); + + // Convert the canvas to a data url (this could + // be displayed inline by inserting it in to an + // tag in the src attribute, ie + // + var theImage = canvas.toDataURL('image/png'); + + // Convert to a blob + var blob = this.dataURLtoBlob(theImage); + + // Prompt to save + saveAs(blob, filename); + +} + +circularTrack.prototype.dataURLtoBlob = function(dataURL) { + // Decode the dataURL + var binary = atob(dataURL.split(',')[1]); + // Create 8-bit unsigned array + var array = []; + for(var i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + // Return our Blob object + return new Blob([new Uint8Array(array)], {type: 'image/png'}); +} + +//////////////////////////////////////////////// +// +// Utility functions +// +//////////////////////////////////////////////// + +circularTrack.prototype.showTrack = function(name) { + var i = this.findTrackbyName(name); + + // We didn't find the track by that name + if(i < 0) { + return; + } + + // Is it already visible? Do nothing + if(this.tracks[i].visible) { + return; + } else { + this.tracks[i].visible = true; + } + + switch(this.tracks[i].trackType) { + case "plot": + this.drawPlot(i, true); + break; + case "track": + this.drawTrack(i, true); + break; + case "stranded": + this.drawTrack(i, true); + break; + case "gap": + this.drawGap(i, true); + break; + case "glyph": + // Do nothing for a glyph type, special case + // but leave this as a placeholder for now + break; + default: + // Do nothing for an unknown track type + } + +} + +circularTrack.prototype.hideTrack = function(name) { + var i = this.findTrackbyName(name); + + // We didn't find the track by that name + if(i < 0) { + return; + } + + // Is it already not visible? Do nothing + if(! this.tracks[i].visible) { + return; + } + + switch(this.tracks[i].trackType) { + case "plot": + this.removePlot(i); + break; + case "track": + this.removeTrack(i); + break; + case "stranded": + this.removeTrack(i); + break; + case "gap": + this.removeGap(i); + break; + case "glyph": + // Do nothing for a glyph type, special case + // but leave this as a placeholder for now + break; + default: + // Do nothing for an unknown track type + } + +} + +circularTrack.prototype.hideGlyphTrackType = function(name, type) { + var i = this.findTrackbyName(name); + + // We didn't find the track by that name + if(i < 0) { + return; + } + + if(this.tracks[i].trackType !== "glyph") { + // Wrong track type, bail + return; + } + + // Don't try to show if already visible + if(! this.isvisibleGlyphTrackType(name, type)) { + return; + } + + for(var j = 0; j < this.tracks[i].visTypes.length; j++) { + if(this.tracks[i].visTypes[j] == type) { + this.tracks[i].visTypes.splice(j, 1); + j--; + } + } + + this.drawGlyphTrack(i); + +} + +circularTrack.prototype.showGlyphTrackType = function(name, type) { + var i = this.findTrackbyName(name); + + // We didn't find the track by that name + if(i < 0) { + return; + } + + if(this.tracks[i].trackType !== "glyph") { + // Wrong track type, bail + return; + } + + // Don't try to show if already visible + if(this.isvisibleGlyphTrackType(name, type)) { + return; + } + + if(! this.tracks[i].visTypes.contains(type) ) { + this.tracks[i].visTypes.push(type); + } + + this.drawGlyphTrack(i); + +} + +circularTrack.prototype.isvisibleGlyphTrackType = function(name, type) { + var i = this.findTrackbyName(name); + + // We didn't find the track by that name + if(i < 0) { + return; + } + + if(this.tracks[i].trackType !== "glyph") { + // Wrong track type, bail + return; + } + + for(var j = 0; j < this.tracks[i].visTypes.length; j++) { + if(this.tracks[i].visTypes[j] == type) { + return true; + } + } + + return false; +} + +circularTrack.prototype.dragresize = function() { + var newWidth = d3.event.x - this.layout.ExtraWidthX; + var newHeight = d3.event.y - this.layout.ExtraWidthY; + + var newSize = Math.max(newWidth, newHeight); + + // Don't allow going below 25px in size + if(newSize <= 25) { + return; + } + +// console.log(this.layout.w); +// console.log("x " + newWidth); +// console.log("y " + newHeight); + + this.layout.newSize = newSize +// this.layout.w = newSize; +// this.layout.h = newSize; + + this.dragbar + .attr("transform", "translate(" + (newSize+this.layout.ExtraWidthX-25) + "," + (newSize+this.layout.ExtraWidthY-25) + ")") + + this.drag_shadow + .attr("width", (newSize+this.layout.ExtraWidthX)) + .attr("height", (newSize+this.layout.ExtraWidthY)); + + if((newSize >= this.layout.w) || (newSize >= this.layout.h)) { + this.container + .attr("width", newSize+this.layout.ExtraWidthX) + .attr("height", newSize+this.layout.ExtraWidthY) + } +// this.resize(newSize); + +} + +circularTrack.prototype.dragresize_start = function() { + d3.event.sourceEvent.stopPropagation(); + + this.drag_shadow + .attr("class", "dragbar-shadow"); +} + +circularTrack.prototype.dragresize_end = function() { + var newSize = this.layout.newSize; + + this.resize(this.layout.w); + + this.layout.w = newSize; + this.layout.w2 = newSize / 2; + this.layout.h = newSize; + this.layout.h2 = this.layout.w2; + + this.drag_shadow + .attr("class", "linear_hidden dragbar-shadow"); + + this.resize(newSize); + +} + +circularTrack.prototype.resize = function(newWidth) { + + var resize_ratio = newWidth / this.layout.radius / 2; + + this.layout.radius = this.layout.factor*Math.min(newWidth/2, newWidth/2); + + this.layout.w = newWidth; + this.layout.w2 = newWidth / 2; + this.layout.h = newWidth; + this.layout.h2 = this.layout.w2; + + this.moveAxis(); + + // Resize the plots + for(var i=0; i < this.tracks.length; i++) { + + // if('undefined' !== typeof this.tracks[i].visible) { + // if(! this.tracks[i].visible) { + // continue; + // } + // } + + // We're going to see what type of tracks we have + // and dispatch them appropriately + + switch(this.tracks[i].trackType) { + case "plot": + this.movePlot(i, (this.tracks[i].plot_radius*resize_ratio)); + break; + case "track": + this.moveTrack(i, (this.tracks[i].inner_radius*resize_ratio), (this.tracks[i].outer_radius*resize_ratio)); + break; + case "stranded": + this.moveTrack(i, (this.tracks[i].inner_radius*resize_ratio), (this.tracks[i].outer_radius*resize_ratio)); + break; + case "gap": + this.moveGap(i, (this.tracks[i].inner_radius*resize_ratio), (this.tracks[i].outer_radius*resize_ratio)); + break; + case "glyph": + this.tracks[i].radius = this.tracks[i].radius * resize_ratio; + // We needed to save the new radius but if this + // track isn't visible, do nothing + if(this.tracks[i].visible) { + this.drawGlyphTrack(i); + } + break; + default: + // Do nothing for an unknown track type + } + } + + this.container + .attr("width", newWidth+this.layout.ExtraWidthX) + .attr("height", newWidth+this.layout.ExtraWidthY) + + this.redrawBrush(this.currentStart, this.currentEnd); + + this.dragbar + .attr("transform", "translate(" + (newWidth+this.layout.ExtraWidthX-25) + "," + (newWidth+this.layout.ExtraWidthY-25) + ")") + +} + +//////////////////////////////////////////////// +// +// Helper functions +// +//////////////////////////////////////////////// + +circularTrack.prototype.findTrackbyName = function(name) { + var tracks = this.tracks; + + for(var i=0; i < tracks.length; i++) { + if(tracks[i].trackName == name) { + return i; + } + } + + return -1; + +} + +circularTrack.prototype.findGlyphTypes = function(i) { + + var classes = []; + + if('undefined' == typeof this.tracks[i].visItems) { + this.tracks[i].visTypes = []; + } + + for(var j=0; j < this.tracks[i].items.length; j++) { + if(! this.tracks[i].visTypes.contains(this.tracks[i].items[j].type)) { + this.tracks[i].visTypes.push(this.tracks[i].items[j].type); + classes.push(this.tracks[i].trackName + "_" + this.tracks[i].items[j].type); + } + } + + this.tracks[i].visClasses = classes.join(' '); + +} + +circularTrack.prototype.maskGlyphTypes = function(i, types) { + + for(var j = this.tracks[i].visTypes.length - 1; j >= 0; j--) { + if(types.contains(this.tracks[i].visTypes[j])) { + this.tracks[i].visTypes.splice(j, 1); + } + } + +} + +Array.prototype.contains = function(obj) { + var i = this.length; + while (i--) { + if (this[i] == obj) { + return true; + } + } + return false; +} + +// If we're displaying a stranded track, calculate +// the inner radius depending on which strand the +// gene is on. + +function calcInnerRadius(inner, outer, strand) { + if('undefined' == typeof strand) { + return inner; + } else if(strand == -1) { + return inner; + } else { + return (inner+outer)/2; + } +} + +// If we're displaying a stranded track, calculate +// the outer radius depending on which strand the +// gene is on. + +function calcOuterRadius (inner, outer, strand) { + if('undefined' == typeof strand) { + return outer; + } else if(strand == -1) { + return (inner+outer)/2; + } else { + return outer; + } +} + +function calcRadBPfromXY (x,y,xScale) { + var rad = Math.PI/2 - Math.atan(y/x); + if(x < 0) { + // II & III quadrant + rad = rad + Math.PI; + } + + return [rad,Math.floor(xScale.invert(rad))]; +} + +function calcMinSliceSize () { + var cfg = this.layout; + + + //cfg.radians_pre_bp +} diff --git a/dammit/viewer/static/linearbrush.js b/dammit/viewer/static/linearbrush.js new file mode 100644 index 00000000..ae5cfefe --- /dev/null +++ b/dammit/viewer/static/linearbrush.js @@ -0,0 +1,97 @@ +var contextMargin = {top: 10, right: 40, bottom: 25, left: 60}, +contextWidth = 440 - contextMargin.left - contextMargin.right, +contextHeight = 100 - contextMargin.top - contextMargin.bottom; + +function linearBrush(layout, callbackObj) { + this.layout; + this.callbackObj = callbackObj; + + this.brushContainer = d3.select(layout.container) + .append("svg") + .attr("width", contextWidth + contextMargin.left + contextMargin.right) + .attr("height", contextHeight + contextMargin.top + contextMargin.bottom) + .attr("class", "contextTracks"); + + this.x1 = d3.scale.linear() + .range([0,contextWidth]) + .domain([0, layout.genomesize]); + + this.mini = this.brushContainer.append("g") + .attr("transform", "translate(" + contextMargin.left + "," + contextMargin.top + ")") + .attr("width", contextWidth) + .attr("height", contextHeight) + .attr("class", "miniBrush"); + + this.brush = d3.svg.brush() + .x(this.x1) + .on("brush", this.brushUpdate.bind(this)); + + // this.brush = brush; + + this.brushContainer = this.mini.append("g") + .attr("class", "track brush") + .call(this.brush.bind(this)) + .selectAll("rect") + .attr("y", 1) + .attr("height", contextHeight - 1); + + this.axisContainer = this.mini.append("g") + .attr('class', 'brushAxis') + .attr("transform", "translate(" + 0 + "," + contextHeight + ")"); + + this.xAxis = d3.svg.axis().scale(this.x1).orient("bottom") + .tickFormat(d3.format("s")); + + this.axisContainer.append("g") + .attr("class", "context axis bottom") + .attr("transform", "translate(0," + 0 + ")") + .call(this.xAxis); + +} + +linearBrush.prototype.brushUpdate = function(b) { + var minExtent = this.brush.extent()[0]; + var maxExtent = this.brush.extent()[1]; + + if( Object.prototype.toString.call( this.callbackObj ) === '[object Array]' ) { + for(var obj in this.callbackObj) { + if(this.callbackObj.hasOwnProperty(obj)) { + this.callbackObj[obj].update(minExtent, maxExtent); + } + } + } else { + this.callbackObj.update(minExtent, maxExtent); + } + +} + +linearBrush.prototype.update = function(startBP, endBP) { + + this.brush.extent([startBP, endBP]); + + d3.selectAll('.brush').call(this.brush); + +} + +linearBrush.prototype.update_finished = function(startBP, endBP) { + +} + +linearBrush.prototype.addBrushCallback = function(obj) { + + // We allow multiple brushes to be associated with a linear plot, if we have + // a brush already, add this new one on. Otherwise just remember it. + + if('undefined' !== typeof this.callbackObj) { + + if( Object.prototype.toString.call( obj ) === '[object Array]' ) { + this.callbackObj.push(obj); + } else { + var tmpobj = this.callbackObj; + this.callbackObj = [tmpobj, obj]; + } + } else { + this.callbackObj = obj; + } + +} diff --git a/dammit/viewer/static/linearplot.js b/dammit/viewer/static/linearplot.js new file mode 100644 index 00000000..dd59e910 --- /dev/null +++ b/dammit/viewer/static/linearplot.js @@ -0,0 +1,1484 @@ +var linearTrackDefaults = { + width: 940, + height: 500, + left_margin: 15, + right_margin: 15, + bottom_margin: 5, + axis_height: 50, + name: "defaultlinear", + dragresize: true +}; + +function genomeTrack(layout,tracks) { + + this.tracks = tracks; + this.layout = layout; + this.numTracks = this.countTracks(); + + if('undefined' !== typeof layout) { + // Copy over any defaults not passed in + // by the user + for(var i in linearTrackDefaults) { + if('undefined' == typeof layout[i]) { + this.layout[i] = linearTrackDefaults[i]; + } + } + } + + if('undefined' == typeof layout.plotid) { + this.layout.plotid = layout.container.slice(1); + } + + this.layout.containerid = layout.container.slice(1); + + this.layout.width_without_margins = + this.layout.width - this.layout.left_margin - + this.layout.right_margin; + + this.layout.height_without_axis = this.layout.height - + this.layout.axis_height; + + this.itemRects = []; + + // Start with showing the entire genome unless otherwise stated + this.visStart = 'undefined' !== typeof layout.initStart ? layout.initStart : 0; + this.visEnd = 'undefined' !== typeof layout.initEnd ? layout.initEnd : layout.genomesize; + + this.x = d3.scale.linear() +// .domain([0, layout.genomesize]) + .domain([this.visStart, this.visEnd]) + .range([0,this.layout.width_without_margins]); + this.x1 = d3.scale.linear() + .range([0,this.layout.width_without_margins]) +// .domain([0, layout.genomesize]); + .domain([this.visStart, this.visEnd]); + this.y1 = d3.scale.linear() + .domain([0,this.numTracks]) + .range([0,(this.layout.height_without_axis-this.layout.bottom_margin)]); + // We need x1 and y1 in the initialization's scope too + // to deal with passing it in to make the lollipops + + this.zoom = d3.behavior.zoom() + .x(this.x1) + .on("zoomstart", function () {d3.event.sourceEvent.preventDefault()} ) + .on("zoom", this.rescale.bind(this)) + .on("zoomend", this.callBrushFinished.bind(this) ); + + if('undefined' == typeof layout.plotid) { + this.layout.plotid = layout.container.slice(1); + } + + d3.select(layout.container).select("svg").remove(); + + this.chart = d3.select(layout.container) + .append("svg") + .attr("id", function() { return layout.container.slice(1) + "_svg"; }) + .attr("width", this.layout.width) + .attr("height", this.layout.height) + .attr("class", "mainTracks") + .call(this.zoom); + + this.defs = this.chart.append("defs"); + this.clipPath = this.defs.append("clipPath") + .attr("id", "trackClip_" + this.layout.containerid) + .append("rect") + .attr("width", this.layout.width_without_margins) + // .attr("width", this.layout.width_without_margins + this.layout.right_margin) + .attr("height", this.layout.height) + .attr("transform", "translate(0,0)"); + // .attr("transform", "translate(" + this.layout.left_margin + ",0)"); + + this.drawFeatures(); + + this.main = this.chart.append("g") + .attr("transform", "translate(" + this.layout.left_margin + ",0)") + .attr("width", this.layout.width_without_margins) + .attr("height", this.layout.height) + .attr("class", "mainTrack"); + + // Resize dragger + if(this.layout.dragresize == true) { + var dragright = d3.behavior.drag() + .on("dragstart", function() { d3.event.sourceEvent.stopPropagation(); }) + .on("drag", this.dragresize.bind(this)); + + this.dragbar_y_mid = this.layout.height/2; + this.dragbar = this.chart.append("g") + .attr("transform", "translate(" + (this.layout.width-this.layout.right_margin) + "," + (this.dragbar_y_mid-10) + ")") + .attr("width", 25) + .attr("height", 20) + .attr("fill", "lightblue") + .attr("fill-opacity", .2) + .attr("cursor", "ew-resize") + .attr("id", "dragbar_" + this.layout.containerid) + .call(dragright); + + this.dragbar.append("rect") + .attr("width", 25) + .attr("height", 20) + .attr("fill-opacity", 0); + + this.dragbar.append("line") + .attr("x1", 6) + .attr("x2", 6) + .attr("y1", 0) + .attr("y2", 20) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 9) + .attr("x2", 9) + .attr("y1", 0) + .attr("y2", 20) + .attr("class", "dragbar-line"); + this.dragbar.append("line") + .attr("x1", 12) + .attr("x2", 12) + .attr("y1", 0) + .attr("y2", 20) + .attr("class", "dragbar-line"); + } + + + this.genomesize = layout.genomesize; + + this.tip = d3.tip() + .attr('class', 'd3-tip') + .offset([-10, 0]) + .html(function(d) { + return "Name: " + d.name + ""; + }); + + this.chart.call(this.tip); + + this.axisContainer = this.chart.append("g") + .attr('class', 'trackAxis') + .attr('width', this.layout.width_without_margins) + .attr("transform", "translate(" + (this.layout.left_margin + 5) + "," + this.layout.height_without_axis + ")"); + + this.xAxis = d3.svg.axis().scale(this.x1).orient("bottom") + .innerTickSize(-this.layout.height) + .outerTickSize(0) + .tickFormat(d3.format("s")); + + this.axisContainer.append("g") + .attr("class", "xaxislinear") + .attr('width', this.layout.width_without_margins) + + .attr("transform", "translate(0," + 10 + ")") + .call(this.xAxis); + + + for(var i=0; i < this.tracks.length; i++) { + // We're going to see what type of tracks we have + // and dispatch them appropriately + + if("undefined" !== typeof this.tracks[i].skipLinear + && this.tracks[i].skipLinear == true) { + continue; + } + + if("undefined" == typeof this.tracks[i].trackFeatures) { + this.tracks[i].trackFeatures = "simple"; + } else if(this.tracks[i].trackFeatures == "complex") { + // We need to pre-calculate the stacking order for + // all arrow type features + + // 0.77 * 0.9 + if(this.tracks[i].trackType == "stranded") { + this.tracks[i].baseheight = (this.y1(1) * 0.693); + } else { + this.tracks[i].baseheight = (this.y1(1) * 0.616); + } + var inframe = []; + this.tracks[i].maxStackOrder = 1; + for(var j = 0; j < this.tracks[i].items.length; j++) { + // If it's the arrow type we're looking for + if("undefined" !== typeof this.tracks[i].items[j].feature && this.tracks[i].items[j].feature == "arrow") { + item = this.tracks[i].items[j]; + // Is there anything else still in frame we need to check? + + this.tracks[i].items[j].stackOrder = 1; + // Next one we encoutner will be 2 + var curr_stackorder = 2; + // Go backwards through the possible inframe elements + // and increase their stack order if needed + for(var k = inframe.length - 1; k >= 0; k--) { + curr_k = inframe[k]; + if(this.tracks[i].items[curr_k].end >= item.start) { + // If the current item in the possible stack + // items is overlapping... + if(this.tracks[i].items[curr_k].stackOrder <= curr_stackorder) { + // If the examined item is below or + // equal to the current stack order + this.tracks[i].items[curr_k].stackOrder = curr_stackorder; + curr_stackorder++; + } + // This takes care of ignoring items that + // are already well above the current stack order + + } else { + // Or if it no longer overlaps, remove it + inframe.splice(k, 1); + } + } + + // Push ourselves on the inframe so the next + // guy can check us out + inframe.push(j); + this.tracks[i].maxStackOrder = Math.max(this.tracks[i].maxStackOrder, curr_stackorder); + // Save the maximum stackorder we've seen + } + } + this.tracks[i].increment = this.y1(0.23) / this.tracks[i].maxStackOrder; + } + + if("undefined" == typeof this.tracks[i].featureThreshold) { + this.tracks[i].featureThreshold = this.genomesize; + } + + switch(this.tracks[i].trackType) { + case "gap": + this.itemRects[i] = this.main.append("g") + .attr("class", this.tracks[i].trackName) + .attr("width", this.layout.width_without_margins) + .attr("clip-path", "url(#trackClip_" + this.layout.containerid + ")"); + this.displayGapTrack(this.tracks[i], i); + break; + case "stranded": + this.itemRects[i] = this.main.append("g") + .attr("class", this.tracks[i].trackName) + .attr("width", this.layout.width_without_margins) + .attr("clip-path", "url(#trackClip_" + this.layout.containerid + ")"); + if('undefined' !== typeof this.tracks[i].linear_skipInit && this.tracks[i].linear_skipInit) { + break; + } + this.displayStranded(this.tracks[i], i); + break; + case "track": + this.itemRects[i] = this.main.append("g") + .attr("class", this.tracks[i].trackName) + .attr("width", this.layout.width_without_margins) + .attr("clip-path", "url(#trackClip_" + this.layout.containerid + ")"); + if('undefined' !== typeof this.tracks[i].linear_skipInit && this.tracks[i].linear_skipInit) { + break; + } + this.displayTrack(this.tracks[i], i); + break; + case "glyph": + if(typeof this.tracks[i].linear_invert !== 'undefined' && this.tracks[i].linear_invert == true) { + this.tracks[i].invert = -1; + } else { + this.tracks[i].invert = 1; + } + if(typeof this.tracks[i].linear_padding !== 'undefined') { + this.tracks[i].padding = this.tracks[i].linear_padding; + } else { + this.tracks[i].padding = 0; + } + this.itemRects[i] = this.main.append("g") + .attr("class", this.tracks[i].trackName) + .attr("width", this.layout.width_without_margins) + .attr("clip-path", "url(#trackClip_" + this.layout.containerid + ")"); + this.displayGlyphTrack(this.tracks[i], i); + break; + case "plot": + this.tracks[i].g = this.itemRects[i] = this.main.append("g") + .attr("class", this.tracks[i].trackName) + .attr("width", this.layout.width_without_margins) + .attr("clip-path", "url(#trackClip_" + this.layout.containerid + ")"); + this.tracks[i].g.append("path") + .attr("class", this.tracks[i].trackName) + .attr("id", this.tracks[i].trackName) + .attr("stroke-width", 1) + .attr("fill", "none"); + + this.displayPlotTrack(this.tracks[i], i); + break; + default: + // Do nothing for an unknown track type + } + } + + // this.main.append("g").attr("transform", "matrix(0.7, 0, 0, 0.7, 0, 0)").append("use").attr("xlink:href", "#lollipop"); + // this.main.append("g").attr("transform", "translate(100,137)").on("click", function() { console.log("click!");}).append("use").attr("xlink:href", "#lollipop_strand_pos"); + +} + +// We can't display all track types, or some don't +// add to the stacking (ie. graph type) + +genomeTrack.prototype.countTracks = function() { + var track_count = 0; + + for(var i=0; i < this.tracks.length; i++) { + + if("undefined" !== this.tracks[i].skipLinear + && this.tracks[i].skipLinear == true) { + continue; + } + + switch(this.tracks[i].trackType) { + case "stranded": + // a linear track counts as two + track_count++; + this.tracks[i].stackNum = track_count; + track_count++; + break; + case "track": + this.tracks[i].stackNum = track_count; + track_count++; + break; + default: + // Do nothing for an unknown track type + } + } + + return track_count; +} + +genomeTrack.prototype.displayStranded = function(track, i) { + var visStart = this.visStart, + visEnd = this.visEnd, + visRange = visEnd - visStart, + x1 = this.x1, + y1 = this.y1; + var cfg = this.layout; + + // Because of how the tooltip library binds to the SVG object we have to turn it + // on or off here rather than in the .on() call, we'll redirect the calls to + // a dummy do-nothing object if we're not showing tips in this context. + var tip = {show: function() {}, hide: function() {} }; + if(('undefined' !== typeof track.showTooltip) && track.showTooltip) { + tip = this.tip; + } + + var stackNum = this.tracks[i].stackNum; + // console.log(visStart, visEnd); + var visItems = track.items.filter(function(d) { + if(typeof d.feature !== 'undefined' && d.feature !== 'gene') { + if(track.featureThreshold < visRange) { + return false; + } + } + return d.start < visEnd && d.end > visStart; + }); + + // console.log(track.items); + + var rects = this.itemRects[i].selectAll("g") + .data(visItems, function(d) { return d.id; }) + .attr("transform", function(d,i) { + return "translate(" + x1(d.start) + ',' + d.yshift + ")"; }); + + // Process the changed/moved rects + rects.selectAll("rect") + .each(function (d) { d.width = x1(d.end + 1) - x1(d.start); }) + .attr("width", function(d) {return d.width;}) + // Yes we really don't need to set the class here again + // except to deal with the _zoom class when zooming + // in and out + .attr("class", function(d) { + return track.trackName + '_' + d.suffix + ' ' + ((d.width > 5) ? (track.trackName + '_' + d.suffix + '_zoomed') : '' ) + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : '');}); + + // Process the text for changed/moved rects + rects.selectAll("text") + .attr("dx", "2px") + .attr("dy", "0.94em") + .each(function (d) { + var bb = this.getBBox(); + var slice_length = x1(d.end) - x1(d.start) - 2; // -2 to offset the dx above + d.visible = (slice_length > bb.width); + }) + .attr("class", function(d) { + return track.trackName + '_text ' + track.trackName + '_' + d.suffix + '_text ' + (d.visible ? '' : "linear_hidden " ) + ('undefined' !== typeof d.extraclass ? d.extraclass : ''); }); + + this.itemRects[i].selectAll(".arrow") + .each(function(d) { + d.width = x1(d.end + 1) - x1(d.start); + + if(d.strand == -1) { + headxtranslate = 0; + arrowline = "m " + d.width + ",0 " + "l 0," + (track.baseheight + d.height) + " -" + d.width + ",0"; + } else { + headxtranslate = d.width; + arrowline = "m 0," + track.baseheight + "l 0,-" + (track.baseheight + d.height) + " " + d.width + ",0"; + } + + d3.select(this).select("path") + .attr("d", function(d) { + return arrowline; + }); + + d3.select(this).select("use") + .attr("transform", function(d) { + return "translate(" + headxtranslate + "," + d.headytranslate + ")"; + }); + + }); + + var entering_rects = rects.enter().append("g") + .attr("transform", function(d,i) { + if(d.strand == -1) { + ystack = stackNum; + d.suffix = 'neg'; + } else if(d.strand == "1") { + ystack = stackNum -1; + d.suffix = 'pos'; + } else { + ystack = stackNum - 0.3; + d.suffix = 'none'; + } + var shift_gene = 0; + if(typeof d.feature !== 'undefined' && d.feature == "terminator") { + ystack = ystack - 0.5; + } else if (track.trackFeatures == 'complex' && d.strand == "1") { + var shift_gene = y1(1) * .2; + + } + d.yshift = y1(ystack) + 10 + shift_gene; + return "translate(" + x1(d.start) + ',' + d.yshift + ")"; }) + // return "translate(" + x1(d.start) + ',' + (y1(ystack) + 10 + shift_gene) + ")"; }) + .attr("id", function(d,i) { return track.trackName + '_' + d.id; }) + .attr("class", function(d) { + return track.trackName + '_' + d.suffix + '_group ' + (typeof d.feature === 'undefined' ? 'gene' : d.feature); })//; + + // entering_rects + .each(function(d) { + d.width = x1(d.end) - x1(d.start); + + if (typeof d.feature === 'undefined' || d.feature == "gene") { + d3.select(this) + .append("rect") + .attr("class", function(d) { + + return track.trackName + '_' + d.suffix + ' ' + ((d.width > 5) ? (track.trackName + '_' + d.suffix + '_zoomed') : '') + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : '');}) + .attr("width", function(d) {return d.width;}) + .attr("height", function(d) { + if(track.trackFeatures == 'complex') { + var scale_factor = 0.77; + } else { + var scale_factor = 1; + } + return (d.strand == 0 ? .4 : .9) * scale_factor * y1(1); + }) + .on("click", function(d,i) { + if (d3.event.defaultPrevented) return; // click suppressed + if('undefined' !== typeof track.linear_mouseclick) { + var fn = window[track.linear_mouseclick]; + if('object' == typeof fn) { + return fn.onclick(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } else { + null; + } + }) + .on('mouseover', function(d) { + tip.show(d); + if('undefined' !== typeof track.linear_mouseover) { + var fn = window[track.linear_mouseover]; + if('object' == typeof fn) { + return fn.mouseover(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }) + .on('mouseout', function(d) { + tip.hide(d); + if('undefined' !== typeof track.linear_mouseout) { + var fn = window[track.linear_mouseout]; + if('object' == typeof fn) { + return fn.mouseout(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }); + + } else if(d.feature == "terminator") { + if(d.strand == 1) { + lollipop = "#lollipop_strand_pos"; + } else { + lollipop = "#lollipop_strand_neg"; + } + d3.select(this).append("use").attr("xlink:href", lollipop); + + } else if(d.feature == "arrow") { + + if(d.strand == -1) { + d.height = y1(0.23) - (d.stackOrder * track.increment); + arrowhead = "#leftarrow"; + headxtranslate = 0; + d.headytranslate = track.baseheight + d.height; + // console.log(track.increment); + // console.log(d.headytranslate); + arrowline = "m " + d.width + ",0 " + "l 0," + (track.baseheight + d.height) + " -" + d.width + ",0"; + // console.log(arrowline); + } else { + d.height = d.stackOrder * track.increment; + arrowhead = "#rightarrow"; + headxtranslate = d.width; + d.headytranslate = d.height * -1; + arrowline = "m 0," + track.baseheight + "l 0,-" + (track.baseheight + d.height) + " " + d.width + ",0"; + + } + arrowclass = track.trackName + '_arrow_' + d.suffix + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : ''); + arrowbase = d3.select(this) + + arrowbase.append("path") + .attr("class", arrowclass) + .attr("d", function(d) { + // 0.77 * 0.9 + + return arrowline; + }) + .attr("fill-opacity", 0) + arrowbase.append("use").attr("xlink:href", arrowhead) + .attr("transform", function(d) { + return "translate(" + headxtranslate + "," + d.headytranslate + ")"; + }) + .attr("class", arrowclass); + + + } //else + }); + + if(('undefined' !== typeof track.showLabels) && typeof track.showLabels) { + entering_rects.each(function(d) { + if(typeof d.feature == 'undefined' || d.feature == 'gene') { + d3.select(this).append("text") + .text(function(d) {return d.name;}) + .attr("dx", "2px") + .attr("dy", "1em") + .each(function (d) { + var bb = this.getBBox(); + var slice_length = x1(d.end - d.start); + d.visible = (slice_length > bb.width); + }) + .attr("class", function(d) { + return track.trackName + '_text ' + track.trackName + '_' + d.suffix + '_text ' + (d.visible ? null : "linear_hidden" ); }); + } + }); + } + + rects.exit().remove(); +} + +genomeTrack.prototype.displayTrack = function(track, i) { + var visStart = this.visStart, + visEnd = this.visEnd, + visRange = visEnd - visStart, + x1 = this.x1, + y1 = this.y1; + var cfg = this.layout; + + // Because of how the tooltip library binds to the SVG object we have to turn it + // on or off here rather than in the .on() call, we'll redirect the calls to + // a dummy do-nothing object if we're not showing tips in this context. + var tip = {show: function() {}, hide: function() {} }; + if(('undefined' !== typeof track.showTooltip) && track.showTooltip) { + tip = this.tip; + } + + var stackNum = this.tracks[i].stackNum; + // console.log(visStart, visEnd, visRange); + var visItems = track.items.filter(function(d) { + if(typeof d.feature !== 'undefined' && d.feature !== 'gene') { + if(track.featureThreshold < visRange) { + return false; + } + } + return d.start < visEnd && d.end > visStart;} + ); + + // console.log(track.items); + + var rects = this.itemRects[i].selectAll("g") + .data(visItems, function(d) { return d.id; }) + .attr("transform", function(d,i) { + return "translate(" + x1(d.start) + ',' + d.yshift + ")"; + }); + + + this.itemRects[i].selectAll("rect") + .each(function (d) { d.width = x1(d.end) - x1(d.start); }) + .attr("width", function(d) {return d.width; }) + // Yes we really don't need to set the class here again + // except to deal with the _zoom class when zooming + // in and out + .attr("class", function(d) {return track.trackName + ' ' + ((d.width > 5) ? (track.trackName + '_zoomed') : '' ) + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : '');}); + + rects.selectAll("text") + .attr("dx", "2px") + .attr("dy", "1em") + .each(function (d) { + var bb = this.getBBox(); + var slice_length = x1(d.end) - x1(d.start) - 2; // -2 to offset the dx above + d.visible = (slice_length > bb.width); + }) + .attr("class", function(d) {return track.trackName + '_text ' + (d.visible ? null : "linear_hidden" ); }); + + this.itemRects[i].selectAll(".arrow") + .each(function(d) { + // console.log(d); + d.width = x1(d.end + 1) - x1(d.start); + + d3.select(this).select("path") + .attr("d", function(d) { + return "m 0," + track.baseheight + "l 0,-" + (track.baseheight + d.height) + " " + d.width + ",0"; + }); + + d3.select(this).select("use") + .attr("transform", function(d) { + return "translate(" + d.width + ",-" + d.headytranslate + ")"; + }); + + }); + + var entering_rects = rects.enter().append("g") + .attr("transform", function(d,i) { + ystack = stackNum; + shift_gene = 0; + if(typeof d.feature !== 'undefined' && d.feature == "terminator") { + ystack = ystack - 0.6; + } else if (track.trackFeatures == 'complex') { + var shift_gene = y1(1) * .175; + + } + + d.yshift = y1(ystack) + 10 + shift_gene; + return "translate(" + x1(d.start) + ',' + d.yshift + ")"; + }) + .attr("id", function(d,i) { return track.trackName + '_' + d.id; }) + .attr("class", function(d) {return track.trackName + '_group ' + (typeof d.feature === 'undefined' ? 'gene' : d.feature); })//; + + // entering_rects + .each(function (d) { + d.width = x1(d.end) - x1(d.start); + + if (typeof d.feature === 'undefined' || d.feature == "gene") { + + d3.select(this).append("rect") + + .attr("class", function(d) {return track.trackName + ' ' + ((d.width > 5) ? (track.trackName + '_zoomed') : '' ) + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : '');}) + .attr("width", function(d) {return d.width; }) + .attr("height", function(d) { + if(track.trackFeatures == 'complex') { + var scale_factor = 0.77; + } else { + var scale_factor = 1; + } + + return .8 * scale_factor * y1(1); + }) + .on("click", function(d,i) { + if (d3.event.defaultPrevented) return; // click suppressed + if('undefined' !== typeof track.linear_mouseclick) { + var fn = window[track.linear_mouseclick]; + if('object' == typeof fn) { + return fn.onclick(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } else { + null; + } + }) + .on('mouseover', function(d) { + tip.show(d); + if('undefined' !== typeof track.linear_mouseover) { + var fn = window[track.linear_mouseover]; + if('object' == typeof fn) { + return fn.mouseover(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }) + .on('mouseout', function(d) { + tip.hide(d); + if('undefined' !== typeof track.linear_mouseout) { + var fn = window[track.linear_mouseout]; + if('object' == typeof fn) { + return fn.mouseout(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }) + + if('undefined' !== typeof track.showLabels) { + // entering_rects + d3.select(this).append("text") + .text(function(d) {return d.name;}) + .attr("dx", "2px") + .attr("dy", "1em") + .each(function (d) { + var bb = this.getBBox(); + var slice_length = x1(d.end) - x1(d.start) -2 ; // -2 to offset the dx above + d.visible = (slice_length > bb.width); + }) + .attr("class", function(d) {return track.trackName + '_text ' + (d.visible ? null : "linear_hidden" ); }); + } + } else if(d.feature == "terminator") { + d3.select(this).append("use").attr("xlink:href", "#lollipop"); + } else if(d.feature == "arrow") { + + d.height = d.stackOrder * track.increment; + d.headytranslate = d.height; + + arrowclass = track.trackName + '_arrow' + ' ' + ('undefined' !== typeof d.extraclass ? d.extraclass : ''); + arrowbase = d3.select(this) + + arrowbase.append("path") + .attr("class", arrowclass) + .attr("d", function(d) { + // 0.77 * 0.9 + return "m 0," + track.baseheight + "l 0,-" + (track.baseheight + d.height) + " " + d.width + ",0"; + + }) + .attr("fill-opacity", 0) + arrowbase.append("use").attr("xlink:href", "#rightarrow") + .attr("transform", function(d) { + return "translate(" + d.width + ",-" + d.headytranslate + ")"; + }) + .attr("class", arrowclass); + + + } //else + }); + + + rects.exit().remove(); +} + +genomeTrack.prototype.displayPlotTrack = function(track, i) { + var visStart = this.visStart, + visEnd = this.visEnd, + x1 = this.x1, + y1 = this.y1; + + if((typeof track.visible == 'undefined') || (track.visible == false)) { + return; + } + + if((typeof track.linear_plot_width == 'undefined') || (typeof track.linear_plot_height == 'undefined')) { +// if((typeof track.linear_plot_width == 'undefined')) { + return; + } + + var startItem = parseInt(visStart / track.bp_per_element); + var endItem = Math.min(parseInt(visEnd / track.bp_per_element), track.items.length); + var offset = ((startItem+1) * track.bp_per_element) - visStart; + + var items = track.items.filter(function(d, i) { return i >= startItem && i <= endItem } ); + + track.plotScale = d3.scale.linear() + .domain([track.plot_min, track.plot_max]) + .range([track.linear_plot_height+(track.linear_plot_width/2), track.linear_plot_height-(track.linear_plot_width/2)]); + + var lineFunction = d3.svg.line() + .x(function(d, i) { return x1((i*track.bp_per_element)); } ) + .y(function(d, i) { return track.plotScale(d); } ) + .interpolate("linear"); + + var plot = this.itemRects[i].selectAll("path") + .attr("d", lineFunction(track.items)) + +// plot.exit().remove(); + +} + +genomeTrack.prototype.displayGapTrack = function(track, i) { + var visStart = this.visStart, + visEnd = this.visEnd, + x1 = this.x1, + y1 = this.y1; + var cfg = this.layout; + var self = this; + + // Because of how the tooltip library binds to the SVG object we have to turn it + // on or off here rather than in the .on() call, we'll redirect the calls to + // a dummy do-nothing object if we're not showing tips in this context. + var tip = {show: function() {}, hide: function() {} }; + if(('undefined' !== typeof track.showTooltip) && track.showTooltip) { + tip = this.tip; + } + + if((typeof track.visible !== 'undefined') && (track.visible == false)) { + return; + } + +// var gap_range = d3.range(0, this.layout.height, 5); + var gap_range = d3.range(0, y1(this.numTracks)+(this.numTracks*3), 5); + + var items = track.items.filter(function(d) {return (d.start <= visEnd && d.start >= visStart) || (d.end <= visEnd && d.end >= visStart);}); + + var gaps = this.itemRects[i].selectAll("path") + .data(items, function(d) { return d.id; }) + .attr("transform", function(d,i) { + return "translate(" + x1(d.start) + ', 0)'; + }) + .each(function (d) { d.width = x1(d.end) - x1(d.start); }) + .attr('d', function(d) { return self.jaggedPathGenerator(d.width, gap_range); } ); + + var entering_gaps = gaps.enter().append("path") + .each(function (d) { d.width = x1(d.end) - x1(d.start); }) + .attr('d', function(d) { return self.jaggedPathGenerator(d.width, gap_range); } ) + .attr("transform", function(d,i) { + return "translate(" + x1(d.start) + ', 0)'; + }) + .attr("id", function(d,i) { return track.trackName + '_' + d.id; }) + .attr("class", function(d) {return track.trackName + ' linearplot ' + (typeof d.feature === 'undefined' ? 'gene' : d.feature); })//; + .on("click", function(d,i) { + if (d3.event.defaultPrevented) return; // click suppressed + if('undefined' !== typeof track.linear_mouseclick) { + var fn = window[track.linear_mouseclick]; + if('object' == typeof fn) { + return fn.onclick(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } else { + null; + } + }) + .on('mouseover', function(d) { + tip.show(d); + if('undefined' !== typeof track.linear_mouseover) { + var fn = window[track.linear_mouseover]; + if('object' == typeof fn) { + return fn.mouseover(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }) + .on('mouseout', function(d) { + tip.hide(d); + if('undefined' !== typeof track.linear_mouseout) { + var fn = window[track.linear_mouseout]; + if('object' == typeof fn) { + return fn.mouseout(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }); + + gaps.exit().remove(); + +} + +genomeTrack.prototype.jaggedPathGenerator = function(width, data) { + + var down = []; + // var up = []; + for(var i = 0; i < data.length; i++) { + var offset = ((i % 2 === 0) ? 3 : -3); + down.push({ x: offset, y: data[i] }); + down.unshift({ x: offset+width, y: data[i] }); + } + down.push(down[0]); + + var generator = d3.svg.line() + .x(function(d,i) { return d.x; }) + .y(function(d,i) { return d.y; }) + .interpolate("linear"); + + return generator(down); + // return generator(down.concat(up)); +} + +genomeTrack.prototype.displayGlyphTrack = function(track, i) { + var visStart = this.visStart, + visEnd = this.visEnd, + x1 = this.x1, + y1 = this.y1; + var cfg = this.layout; + + if((typeof track.visible !== 'undefined') && (track.visible == false)) { + return; + } + + // Because of how the tooltip library binds to the SVG object we have to turn it + // on or off here rather than in the .on() call, we'll redirect the calls to + // a dummy do-nothing object if we're not showing tips in this context. + var tip = {show: function() {}, hide: function() {} }; + if(('undefined' !== typeof track.showTooltip) && track.showTooltip) { + tip = this.tip; + } + + var items = track.items.filter(function(d) {return d.bp <= visEnd && d.bp >= visStart;}); + + // When we move we need to recalculate the stacking order + var stackCount = 0; + for(var j = 0; j < items.length; j++) { + if(items[j].bp < visStart || items[j].bp > visEnd) { + continue; + } + if(j < 1) { + items[j].stackCount = 0; + continue; + } + + var dist = x1(items[j].bp) - x1(items[j-1].bp); + + if(dist < track.linear_pixel_spacing) { + items[j].stackCount = items[j-1].stackCount + 1; + continue; + } + + items[j].stackCount = 0; + } + + // Because SVG coordinates are from the top-left, the "height" is pixels DOWN from + // the top of the image to start stacking the glyphs + + var glyphs = this.itemRects[i].selectAll("path") + .data(items, function(d) { return d.id; }) + .attr("transform", function(d,i) { return "translate(" + (x1(d.bp) + track.padding) + ',' + (track.linear_height - (track.linear_glyph_buffer * d.stackCount * track.invert)) + ")"; }); + + var entering_glyphs = glyphs.enter() + .append('path') + .attr('id', function(d,i) { return track.trackName + "_glyph" + d.id; }) + .attr('class', function(d) {return track.trackName + '_' + d.type + " linear_" + track.trackName + '_' + d.type; }) + .attr("d", d3.svg.symbol().type(track.glyphType).size(track.linear_glyphSize)) + .attr("transform", function(d,i) { return "translate(" + (x1(d.bp) + track.padding) + ',' + (track.linear_height - (track.linear_glyph_buffer * d.stackCount * track.invert)) + ")"; }) + .on("click", function(d,i) { + if('undefined' !== typeof track.linear_mouseclick) { + var fn = window[track.linear_mouseclick]; + if('object' == typeof fn) { + return fn.onclick(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } else { + null; + } + }) + .on('mouseover', function(d) { + tip.show(d); + if('undefined' !== typeof track.linear_mouseover) { + var fn = window[track.linear_mouseover]; + if('object' == typeof fn) { + return fn.mouseover(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }) + .on('mouseout', function(d) { + tip.hide(d); + if('undefined' !== typeof track.linear_mouseout) { + var fn = window[track.linear_mouseout]; + if('object' == typeof fn) { + return fn.mouseout(track.trackName, d, cfg.plotid); + } else if('function' == typeof fn) { + return fn(track.trackName, d, cfg.plotid); + } + } + }); + + glyphs.exit() + .remove(); + +} + +genomeTrack.prototype.displayAxis = function() { + this.axisContainer.select(".xaxislinear").call(this.xAxis); +} + + genomeTrack.prototype.update = function(startbp, endbp, params) { + // console.log(startbp, endbp); + + this.visStart = startbp; + this.visEnd = endbp; + + this.zoom.x(this.x1.domain([startbp,endbp])); + + this.redraw(); +} + +genomeTrack.prototype.update_finished = function(startbp, endbp, params) { + // console.log("Thank you, got: " + startbp, endbp); + +} + +genomeTrack.prototype.resize = function(newWidth) { + this.layout.width = newWidth; + + this.dragbar + .attr("transform", "translate(" + (newWidth - + this.layout.right_margin) + "," + (this.dragbar_y_mid-15) + ")") + + this.layout.width_without_margins = + this.layout.width - this.layout.left_margin - + this.layout.right_margin; + + this.x + .range([0,this.layout.width_without_margins]); + this.x1 + .range([0,this.layout.width_without_margins]); + + this.chart + .attr("width", this.layout.width) + + this.clipPath + .attr("width", this.layout.width_without_margins) + + this.main + .attr("width", this.layout.width_without_margins) + + this.redraw(); + +} + +genomeTrack.prototype.dragresize = function(d) { + var newWidth = d3.event.x; + + // console.log(this.layout.containerid); + this.resize(newWidth); + // d3.event.preventDefault(); + +} + +genomeTrack.prototype.redraw = function() { + + for(var i = 0; i < this.tracks.length; i++) { + + if("undefined" !== this.tracks[i].skipLinear + && this.tracks[i].skipLinear == true) { + continue; + } + + switch(this.tracks[i].trackType) { + case 'gap': + this.displayGapTrack(this.tracks[i], i); + break; + case "stranded": + this.displayStranded(this.tracks[i], i); + break; + case "track": + this.displayTrack(this.tracks[i], i); + break; + case "glyph": + this.displayGlyphTrack(this.tracks[i], i); + break; + case "plot": + this.displayPlotTrack(this.tracks[i], i); + break; + default: + // Do nothing for an unknown track type + } + } + + this.axisContainer.select(".xaxislinear").call(this.xAxis); + +} + +genomeTrack.prototype.rescale = function() { + var cfg = this.layout; + + var reset_s = 0; + if ((this.x1.domain()[1] - this.x1.domain()[0]) >= (this.genomesize - 0)) { + this.zoom.x(this.x1.domain([0, this.genomesize])); + reset_s = 1; + } + + if (reset_s == 1) { // Both axes are full resolution. Reset. + this.zoom.scale(1); + this.zoom.translate([0,0]); + } + else { + if (this.x1.domain()[0] < 0) { + this.x1.domain([0, this.x1.domain()[1] - this.x1.domain()[0] + 0]); + } + if (this.x1.domain()[1] > this.genomesize) { + var xdom0 = this.x1.domain()[0] - this.x1.domain()[1] + this.genomesize; + this.x1.domain([xdom0, this.genomesize]); + } + } + + var cur_domain = this.x1.domain(); + this.visStart = cur_domain[0]; + this.visEnd = cur_domain[1]; + + if('undefined' !== typeof this.callbackObj) { + if( Object.prototype.toString.call( this.callbackObj ) === '[object Array]' ) { + for(var obj in this.callbackObj) { + if(this.callbackObj.hasOwnProperty(obj)) { + this.callbackObj[obj].update(this.x1.domain()[0], this.x1.domain()[1], { plotid: cfg.plotid } ); + } + } + } else { + this.callbackObj.update(this.x1.domain()[0], this.x1.domain()[1], { plotid: cfg.plotid } ); + } + } + + this.redraw(); + +} + +genomeTrack.prototype.addBrushCallback = function(obj) { + + // We allow multiple brushes to be associated with a linear plot, if we have + // a brush already, add this new one on. Otherwise just remember it. + + if('undefined' !== typeof this.callbackObj) { + + if( Object.prototype.toString.call( obj ) === '[object Array]' ) { + this.callbackObj.push(obj); + } else { + var tmpobj = this.callbackObj; + this.callbackObj = [tmpobj, obj]; + } + } else { + this.callbackObj = obj; + } + + // And make sure our new brush is updated to reflect + // the current visible area + obj.update(this.visStart, this.visEnd); +} + +genomeTrack.prototype.callBrushFinished = function() { + var cfg = this.layout; + + if('undefined' !== typeof this.callbackObj) { + if( Object.prototype.toString.call( this.callbackObj ) === '[object Array]' ) { + for(var obj in this.callbackObj) { + if(this.callbackObj.hasOwnProperty(obj)) { + this.callbackObj[obj].update_finished(this.x1.domain()[0], this.x1.domain()[1], { plotid: cfg.plotid } ); + } + } + } else { + this.callbackObj.update_finished(this.x1.domain()[0], this.x1.domain()[1], { plotid: cfg.plotid } ); + } + } + +} + +//////////////////////////////////////////////// +// +// Export functionality +// +//////////////////////////////////////////////// + +// Allowed export formats are 'png' and 'svg' +// The extension will be added, just summply the base +// filename. + +// Saving to raster format is dependent on FileSaver.js +// and canvg.js (which include rgbcolor.js & StackBlur.js), +// they must be loaded before circularplot.js + +genomeTrack.prototype.savePlot = function(scaling, filename, stylesheetfile, format) { + // First lets get the stylesheet + var sheetlength = stylesheetfile.length; + var style = document.createElementNS("http://www.w3.org/1999/xhtml", "style"); + style.textContent += ""; + + // Now we clone the SVG element, resize and scale it up + var container = this.layout.container.slice(1); + var containertag = document.getElementById(container); + var clonedSVG = containertag.cloneNode(true); + var svg = clonedSVG.getElementsByTagName("svg")[0]; + + // Remove any hidden elements such as text that's not being shown + var tags = svg.getElementsByClassName("linear_hidden") + for(var i=tags.length-1; i>=0; i--) { +// if(tags[i].getAttributeNS(null, "name") === name) { + tags[i].parentNode.removeChild(tags[i]); +// } + } + + // We need to resize the svg with the new canvas size + svg.removeAttribute('width'); + svg.removeAttribute('height'); + svg.setAttribute('width', this.layout.width*scaling); + svg.setAttribute('height', this.layout.height*scaling); + + // Update first g tag with the scaling + g = svg.getElementsByTagName("g")[0]; + transform = g.getAttribute("transform"); +// g.setAttribute("transform", transform + " scale(" + scaling + ")"); + + // Append the stylehsheet to the cloned svg element + // so when we export it the style are inline and + // get rendered + svg.getElementsByTagName("defs")[0].appendChild(style); + + // Fetch the actual SVG tag and convert it to a canvas + // element + var content = clonedSVG.innerHTML.trim(); + + if(format == 'svg') { + var a = document.createElement('a'); + a.href = "data:application/octet-stream;base64;attachment," + btoa(content); + a.download = filename + ".svg"; + a.click(); + + } else if(format == 'png') { + var canvas = document.createElement('canvas'); + canvg(canvas, content); + + // Convert the canvas to a data url (this could + // be displayed inline by inserting it in to an + // tag in the src attribute, ie + // + var theImage = canvas.toDataURL('image/png'); + + // Convert to a blob + var blob = this.dataURLtoBlob(theImage); + + // Prompt to save + saveAs(blob, filename); + } +} + +genomeTrack.prototype.dataURLtoBlob = function(dataURL) { + // Decode the dataURL + var binary = atob(dataURL.split(',')[1]); + // Create 8-bit unsigned array + var array = []; + for(var i = 0; i < binary.length; i++) { + array.push(binary.charCodeAt(i)); + } + // Return our Blob object + return new Blob([new Uint8Array(array)], {type: 'image/png'}); +} + +genomeTrack.prototype.drawFeatures = function() { + var x1 = this.x1; + var y1 = this.y1; + + // Quick debugging variable to show click rects + var opacity = 0; + + // Lollipop for terminator glyph (stranded, positive) + var lollipop_strand_pos = this.defs.append("g").attr("id", "lollipop_strand_pos"); + lollipop_strand_pos.append("path") + .attr("class", "lollipophead") + .attr("d", function() { + arc = "m 0,"; + arc += y1(1) * 0.69; + arc += " a "; + arc += y1(1) * 0.13; + arc += ","; + arc += y1(1) * 0.13; + arc += " 0 1 1 "; + arc += y1(1) * 0.09; + arc += ",0"; + // console.log(arc); + return arc; + // "m 0,60 a 12,12 0 1 1 8,0" + }); + lollipop_strand_pos.append("path") + .attr("class", "lollipopstemend") + .attr("d", function() { + line = "m " + (y1(1) * 0.09) + ","; + line += y1(1) * 0.69; + line += " l 0,"; + line += y1(1) * 0.70; + // console.log("line " + line); + return line; + // "m 8,60 l 0,65" + }); + lollipop_strand_pos.append("path") + .attr("class", "lollipopstemstart") + .attr("d", function() { + line = "m 0,"; + line += y1(1) * 0.69; + line += " l 0,"; + line += y1(1) * 0.70; + // console.log(line); + return line; + // "m 0,60 l 0,65" + }); + lollipop_strand_pos.append("rect") + .attr("transform", function () { + return "translate(-" + (y1(1) * 0.1) + "," + (y1(1) * 0.43) + ")"; + }) + .attr("width", function() { + return y1(1) * 0.3; + }) + .attr("height", function() { + return y1(1) * 0.26; + }) + .attr("fill-opacity", opacity); + lollipop_strand_pos.append("rect") + .attr("transform", function() { + return "translate(0, " + (y1(1) * 0.68) + ")"; + // "translate(0,60)" + }) + .attr("width", 8) + .attr("height", function() { + return y1(1) * 0.72; + }) + .attr("fill-opacity", opacity); + + // Lollipop for terminator glyph (stranded, negative) + var lollipop_strand_neg = this.defs.append("g").attr("id", "lollipop_strand_neg"); + lollipop_strand_neg.append("path") + .attr("class", "lollipophead") + .attr("d", function() { + arc = "m 0,"; + arc += y1(1) * 1.20; + arc += " a "; + arc += y1(1) * 0.13; + arc += ","; + arc += y1(1) * 0.13; + arc += " 0 1 0 "; + arc += y1(1) * 0.09; + arc += ",0"; + // console.log(arc); + return arc; + // "m 0,60 a 12,12 0 1 1 8,0" + }); + lollipop_strand_neg.append("path") + .attr("class", "lollipopstemstart") + .attr("d", function() { + line = "m " + (y1(1) * 0.09) + ","; + line += y1(1) * 1.20; + line += " l 0,"; + line += y1(1) * 0.71 * -1; + // console.log(line); + return line; + // "m 8,60 l 0,65" + }); + lollipop_strand_neg.append("path") + .attr("class", "lollipopstemend") + .attr("d", function() { + line = "m 0,"; + line += y1(1) * 1.20; + line += " l 0,"; + line += y1(1) * 0.71 * -1; + // console.log(line); + return line; + // "m 0,60 l 0,65" + }); + lollipop_strand_neg.append("rect") + .attr("transform", function () { + return "translate(-" + (y1(1) * 0.1) + "," + (y1(1) * 1.20) + ")"; + }) + .attr("width", function() { + return y1(1) * 0.3; + }) + .attr("height", function() { + return y1(1) * 0.26; + }) + .attr("fill-opacity", opacity); + lollipop_strand_neg.append("rect") + .attr("transform", function() { + return "translate(0, " + (y1(1) * 0.50) + ")"; + // "translate(0,60)" + }) + .attr("width", 8) + .attr("height", function() { + return y1(1) * 0.71; + }) + .attr("fill-opacity", opacity); + + // Lollipop for terminator glyph (unstranded) + var lollipop = this.defs.append("g").attr("id", "lollipop"); + lollipop.append("path") + .attr("class", "lollipophead") + .attr("d", function() { + arc = "m 0,"; + arc += y1(1) * 0.77; + arc += " a "; + arc += y1(1) * 0.13; + arc += ","; + arc += y1(1) * 0.13; + arc += " 0 1 1 "; + arc += y1(1) * 0.09; + arc += ",0"; + // console.log(arc); + return arc; + // "m 0,60 a 12,12 0 1 1 8,0" + }); + lollipop.append("path") + .attr("class", "lollipopstemend") + .attr("d", function() { + line = "m " + (y1(1) * 0.09) + ","; + line += y1(1) * 0.77; + line += " l 0,"; + line += y1(1) * 0.62; + // console.log(line); + return line; + // "m 8,60 l 0,65" + }); + lollipop.append("path") + .attr("class", "lollipopstemstart") + .attr("d", function() { + line = "m 0,"; + line += y1(1) * 0.77; + line += " l 0,"; + line += y1(1) * 0.62; + // console.log(line); + return line; + // "m 0,60 l 0,65" + }); + lollipop.append("rect") + .attr("transform", function () { + return "translate(-" + (y1(1) * 0.1) + "," + (y1(1) * 0.51) + ")"; + }) + .attr("width", function() { + return y1(1) * 0.3; + }) + .attr("height", function() { + return y1(1) * 0.26; + }) + .attr("fill-opacity", opacity); + lollipop.append("rect") + .attr("transform", function() { + return "translate(0, " + (y1(1) * 0.76) + ")"; + // "translate(0,60)" + }) + .attr("width", 8) + .attr("height", function() { + return y1(1) * 0.64; + }) + .attr("fill-opacity", opacity); + + // Right arrow + var rightarrow = this.defs.append("g").attr("id", "rightarrow"); + rightarrow.append("path") + .attr("class", "rightarrow") + .attr("d", function() { + return "m 0,0 l -4,4 m 0,-8 l 4,4"; + }); + + // Left arrow + var leftarrow = this.defs.append("g").attr("id", "leftarrow"); + leftarrow.append("path") + .attr("class", "rightarrow") + .attr("d", function() { + return "m 0,0 l 4,4 m 0,-8 l -4,4"; + // return "m 0," + y1(1) + " l 4,4 m 0,-8 l -4,4"; + }); + +} diff --git a/dammit/viewer/static/mygene_autocomplete_jqueryui.js b/dammit/viewer/static/mygene_autocomplete_jqueryui.js new file mode 100644 index 00000000..33c13141 --- /dev/null +++ b/dammit/viewer/static/mygene_autocomplete_jqueryui.js @@ -0,0 +1,120 @@ +/*! jquery.ui.genequery_autocomplete.js + * A JQuery UI widget for gene query autocomplete + * This autocomplete widget for gene query provides suggestions while you type a gene + * symbol or name into the field. By default the gene suggestions are displayed as + * ":", automatically triggered when at least two characters are entered + * into the field. + * Copyright (c) 2013 Chunlei Wu; Apache License, Version 2.0. + */ + + +//Ref: http://stackoverflow.com/questions/1038746/equivalent-of-string-format-in-jquery +//Example usage: alert("Hello {name}".format({ name: 'World' })); +String.prototype.format = function (args) { + var newStr = this; + for (var key in args) { + var regex = new RegExp('{'+key+'}', "igm"); + newStr = newStr.replace(regex, args[key]); + } + return newStr; +}; + +//Subclass default jQuery UI autocomplete widget +//Ref: http://stackoverflow.com/questions/5218300/how-do-subclass-a-widget +//Ref: https://gist.github.com/962848 +$.widget("my.genequery_autocomplete", $.ui.autocomplete, { + + options: { + mygene_url: 'http://mygene.info/v2/query', + //exact match with symbol is boosted. + q: "(symbol:{term} OR symbol: {term}* OR name:{term}* OR alias: {term}* OR summary:{term}*)", +// q: "{term}*", + species: "human", + fields: "name,symbol,taxid,entrezgene", + limit:20, + gene_label: "{symbol}: {name}", + value_attr: 'symbol', + minLength: 2 + }, + + _create: function() { + //this._super("_create"); + $.ui.autocomplete.prototype._create.call(this); + var self = this; + var _options = self.options; + + this.source = function( request, response ) { + $.ajax({ + url: _options.mygene_url, + dataType: "jsonp", + jsonp: 'callback', + data: { + q: _options.q.format({term:request.term}), + sort:_options.sort, + limit:_options.limit, + fields: _options.fields, + species: _options.species + }, + success: function( data ) { + var species_d = {3702: 'thale-cress', + 6239: 'nematode', + 7227: 'fruitfly', + 7955: 'zebrafish', + 8364: 'frog', + 9606: 'human', + 9823: 'pig', + 10090: 'mouse', + 10116: 'rat'}; + if (data.total > 0){ + response( $.map( data.hits, function( item ) { + var obj = { + id: item._id, + value: item[_options.value_attr] + } + $.extend(obj, item); + if (species_d[obj.taxid]){ + obj.species = species_d[obj.taxid]; + } + obj.label = _options.gene_label.format(obj); + return obj; + })); + }else{ + response([{label:'no matched gene found.', value:''}]); + } + } + }); + }; + + //set default title attribute if not set already. + if (this.element.attr("title") === undefined){ + this.element.attr("title", 'Powered by mygene.info'); + } + //set default select callback if not provided. + _options.select = _options.select || this._default_select_callback; + + //set default loading icon + this._add_css('.ui-autocomplete-loading { background: white url("'+this._url_root+'img/ui-anim_basic_16x16.gif") right center no-repeat; }'); + + }, + + _url_root : 'http://mygene.info/widget/autocomplete/', + + //helper function for adding custom css style. + _add_css : function(cssCode) { + var styleElement = document.createElement("style"); + styleElement.type = "text/css"; + if (styleElement.styleSheet) { + styleElement.styleSheet.cssText = cssCode; + } else { + styleElement.appendChild(document.createTextNode(cssCode)); + } + document.getElementsByTagName("head")[0].appendChild(styleElement); + }, + + _default_select_callback: function(event, ui) { + alert( ui.item ? + "Selected: " + ui.item.label + '('+ui.item.id+')': + "Nothing selected, input was " + this.value); + } + +}); diff --git a/dammit/viewer/static/rgbcolor.js b/dammit/viewer/static/rgbcolor.js new file mode 100644 index 00000000..04423f29 --- /dev/null +++ b/dammit/viewer/static/rgbcolor.js @@ -0,0 +1,288 @@ +/** + * A class to parse color values + * @author Stoyan Stefanov + * @link http://www.phpied.com/rgb-color-parser-in-javascript/ + * @license Use it if you like it + */ +function RGBColor(color_string) +{ + this.ok = false; + + // strip any leading # + if (color_string.charAt(0) == '#') { // remove # if any + color_string = color_string.substr(1,6); + } + + color_string = color_string.replace(/ /g,''); + color_string = color_string.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + var simple_colors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred : 'cd5c5c', + indigo : '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + for (var key in simple_colors) { + if (color_string == key) { + color_string = simple_colors[key]; + } + } + // emd of simple type-in colors + + // array of color definition objects + var color_defs = [ + { + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function (bits){ + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]) + ]; + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process: function (bits){ + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process: function (bits){ + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } + ]; + + // search through the definitions to find a match + for (var i = 0; i < color_defs.length; i++) { + var re = color_defs[i].re; + var processor = color_defs[i].process; + var bits = re.exec(color_string); + if (bits) { + channels = processor(bits); + this.r = channels[0]; + this.g = channels[1]; + this.b = channels[2]; + this.ok = true; + } + + } + + // validate/cleanup values + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + + // some getters + this.toRGB = function () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + this.toHex = function () { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length == 1) r = '0' + r; + if (g.length == 1) g = '0' + g; + if (b.length == 1) b = '0' + b; + return '#' + r + g + b; + } + + // help + this.getHelpXML = function () { + + var examples = new Array(); + // add regexps + for (var i = 0; i < color_defs.length; i++) { + var example = color_defs[i].example; + for (var j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + for (var sc in simple_colors) { + examples[examples.length] = sc; + } + + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (var i = 0; i < examples.length; i++) { + try { + var list_item = document.createElement('li'); + var list_color = new RGBColor(examples[i]); + var example_div = document.createElement('div'); + example_div.style.cssText = + 'margin: 3px; ' + + 'border: 1px solid black; ' + + 'background:' + list_color.toHex() + '; ' + + 'color:' + list_color.toHex() + ; + example_div.appendChild(document.createTextNode('test')); + var list_item_value = document.createTextNode( + ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() + ); + list_item.appendChild(example_div); + list_item.appendChild(list_item_value); + xml.appendChild(list_item); + + } catch(e){} + } + return xml; + + } + +} + diff --git a/dammit/viewer/static/test_tracks.js b/dammit/viewer/static/test_tracks.js new file mode 100644 index 00000000..f856ef03 --- /dev/null +++ b/dammit/viewer/static/test_tracks.js @@ -0,0 +1,92 @@ +var tracks = [ + { + "inner_radius": 100, + "trackName": "dammit.HMMER", + "showTooltip": true, + "items": [ + { + "start": 3583, + "end": 4095, + "id": 0, + "name": "DEAD/DEAH box helicase" + }, + { + "start": 4189, + "end": 4551, + "id": 1, + "name": "Helicase conserved C-terminal domain" + }, + { + "start": 5206, + "end": 5310, + "id": 2, + "name": "Helicase conserved C-terminal domain" + } + ], + "visible": true, + "outer_radius": 140, + "min_slice": true, + "trackType": "track", + "trackFeatures": "complex" + }, + { + "inner_radius": 150, + "trackName": "dammit.LAST", + "showTooltip": true, + "items": [ + { + "start": 1, + "end": 5663, + "id": 0, + "name": "TLH2_SCHPO" + } + ], + "visible": true, + "outer_radius": 190, + "min_slice": true, + "trackType": "track", + "trackFeatures": "complex" + }, + { + "inner_radius": 200, + "trackName": "transdecoder", + "showTooltip": true, + "items": [ + { + "start": 1, + "end": 5662, + "id": 0, + "name": "cds.Transcript_0|m.1" + }, + { + "start": 1, + "end": 5663, + "id": 1, + "name": "Transcript_0|m.1.exon1" + }, + { + "start": 1, + "end": 5663, + "id": 2, + "name": "ORF%20Transcript_0%7Cg.1%20Transcript_0%7Cm.1%20type%3A3prime_partial%20len%3A1888%20%28%2B%29" + }, + { + "start": 1, + "end": 5663, + "id": 3, + "name": "ORF%20Transcript_0%7Cg.1%20Transcript_0%7Cm.1%20type%3A3prime_partial%20len%3A1888%20%28%2B%29" + }, + { + "start": 5662, + "end": 5663, + "id": 4, + "name": "Transcript_0|m.1.utr3p1" + } + ], + "visible": true, + "outer_radius": 240, + "min_slice": true, + "trackType": "track", + "trackFeatures": "complex" + } +] diff --git a/dammit/viewer/static/tracks.css b/dammit/viewer/static/tracks.css new file mode 100644 index 00000000..5bd0426e --- /dev/null +++ b/dammit/viewer/static/tracks.css @@ -0,0 +1,286 @@ +/* Classes for the demo's controls */ +#demo-controls { + padding: 10px; +} + +.control-item { + padding: 5px; +} + +/* Render the chart as cleanly as possible */ +#circularchart { + shape-rendering: geometricPrecision; +} + +/* Text on the demo */ +.mini text { + font: 9px sans-serif; +} + +.main text { + font: 12px sans-serif; +} + +/* Definitions for the plot resize dragger, and + resizing shadow */ +.dragbar-line { + stroke-width: 1; + stroke: lightgrey; + pointer-events: inherit; +} + +.dragbar-shadow { + stroke-width: 1; + stroke: lightgrey; + fill: transparent; +} + +/* Style for the plot move arrow if active */ +.move-cross { + stroke-width: 1; + stroke: lightgrey; + fill: lightgrey; + cursor:move; +} + +.move-shadow { + cursor:move !important; +} + +/* For resizing the linear chart */ +#linearchart { + cursor: -webkit-grab; cursor: -moz-grab; +} + +#linearchart:active { + cursor: -webkit-grabbing; cursor: -moz-grabbing; +} + +.mainTrack { + cursor: pointer; + cursor: hand; +} + +/* Definitions for first stranded track, colour, stroke */ +."dammit.HMMER"_pos { + fill: darksalmon; + stroke: #d1876d; + stroke-opacity: 0.8; + stroke-width: 1; +} + +."dammit.HMMER"_neg { + fill: #FFCC00; + fill-opacity: .7; + /*stroke-width: 1; + stroke: #4c602a;*/ +} + +/* In a linear track if an intergenic + region is defined, give it a colour */ +."dammit.HMMER"_none { + fill: #cccccc; + fill-opacity: .7; + stroke-width: 1; +} + +/* And we want to show the user as they hover + over a track, change the colour */ +."dammit.HMMER"_pos:hover { + fill: red; + stroke-width: 1; + stroke: 1; +} + +."dammit.HMMER"_neg:hover { + fill: red; + fill-opacity: .9; + stroke-width: 1; + stroke: red; +} + +/* In a linear track, if there are regulatory + elements such as transcription factors and + terminators, give them a colour for the stroke */ +."dammit.HMMER"_arrow_pos, ."dammit.HMMER"_arrow_neg, ."dammit.LAST"_arrow { + stroke-width: 1; + stroke: black; +} + +/* Labels on the linear track */ +."dammit.HMMER"_text { + fill: white; + font-weight: bold; + font: 12px arial; +} + +/* Track 2 is also a stranded track, give it + a different set of colours */ +.transdecoder_pos { + fill: #000099; + fill-opacity: .7; +} + +/* When a linear track is zoomed to the point + it's text is visible an extra class is added + to the element we can hook in to */ +.transdecoder_pos_zoomed { + stroke-width: 1px; + stroke: #647381; +} + +.transdecoder_neg { + fill: #006600; + fill-opacity: .7; +} + +.transdecoder_neg_zoomed { + stroke-width: 1px; + stroke: #007300; + stroke-opacity: 0.7; +} + +.transdecoder_text { + fill: white; + font-weight: bold; + font: 12px arial; +} + +/* Track 3 is a non-stranded track, so no _pos + and _neg modifiers on the classes */ +.dammit\.LAST { + fill: #3399FF; + fill-opacity: .7; +} + +.dammit\.LAST:hover { + fill: blue; + fill-opacity: .9; +} + +.dammit\.LAST_text { + fill: white; + font-weight: bold; + font: 12px arial; +} + +/* The GC Plot type track, give the stroke a colour */ +.gcplot { + stroke: black; +} + +/* Track 5 is the glyphs, classes of a concatenation + of the track name and the type of glyph in the track */ +.track5_vfdb { + fill: #663300; +} + +.track5_adb { + fill: #FF9933; +} + +/* Give a colour to the glyph track's stroke */ +.gapTrack { + stroke: grey; +} + +/* Set the stroke width of the axis lines */ +.trackAxis .domain { + stroke-width: 1; +} + +/* In a linear track, we need to set the + colours for the drag brush */ +.brush .background { + stroke: black; + stroke-width: 2; + fill: slategray; + fill-opacity: .3; +} + +.brush .extent { + stroke: gray; + fill: dodgerblue; + fill-opacity: .365; +} + +/* In a circular plot if there's a + drag brush for zoomed region, define the + colours and style */ +.polarbrush { + fill: lightgrey; + opacity: 0.5; +} + +/* In the circular plot, the circular end points + for the drag brush */ +.brushEnd { + fill: "red"; +} + +.brushStart { + fill: "green"; +} + +/* If we need to set an element to hidden, give + ourselves a class */ +.linear_hidden { + visibility: hidden; +} + +/* For regulatory elements in a linear plot, + define the colours and stroke for the lollipop + head (terminator site) */ +.lollipophead { + stroke:#000000; + stroke-opacity: 1; + fill: none; +} + +.lollipopstemstart { + fill:none; + stroke:#000000; + stroke-width: 1px; + stroke-linecap:butt; + stroke-linejoin:miter; + stroke-dasharray: 5,5; +} + +.lollipopstemend { + fill:none; + stroke:#000000; + stroke-width: 1px; + stroke-linecap:butt; + stroke-linejoin:miter; +} + +/* For our tooltips, how should we display them? */ +.d3-tip { + line-height: 1; + font: 12px arial; + font-weight: bold; + padding: 12px; + background: rgba(0, 0, 0, 0.8); + color: #fff; + border-radius: 2px; +} + +/* Creates a small triangle extender for the tooltip */ +.d3-tip:after { + box-sizing: border-box; + display: inline; + font-size: 10px; + width: 100%; + line-height: 1; + color: rgba(0, 0, 0, 0.8); + content: "\25BC"; + position: absolute; + text-align: center; +} + +/* Style northward tooltips differently */ +.d3-tip.n:after { + margin: -1px 0 0 0; + top: 100%; + left: 0; +} diff --git a/dammit/viewer/templates/404.html b/dammit/viewer/templates/404.html new file mode 100644 index 00000000..f1e6b5cb --- /dev/null +++ b/dammit/viewer/templates/404.html @@ -0,0 +1,6 @@ + + +

dammit, 404 error!

+

{{ msg }}

+ + diff --git a/dammit/viewer/templates/index.html b/dammit/viewer/templates/index.html new file mode 100644 index 00000000..647853eb --- /dev/null +++ b/dammit/viewer/templates/index.html @@ -0,0 +1,38 @@ + + + + + Annotation Report, Dammit + + + + + + + + + + + + +
+

Annotation Report, Dammit

+

for {{ name }}

+
+ + {% include 'transcript_model.html' %} + + diff --git a/dammit/viewer/templates/js_imports.html b/dammit/viewer/templates/js_imports.html new file mode 100644 index 00000000..d1452cc6 --- /dev/null +++ b/dammit/viewer/templates/js_imports.html @@ -0,0 +1,2 @@ + + diff --git a/dammit/viewer/templates/search.html b/dammit/viewer/templates/search.html new file mode 100644 index 00000000..5b08a4f5 --- /dev/null +++ b/dammit/viewer/templates/search.html @@ -0,0 +1,5 @@ + + + diff --git a/dammit/viewer/templates/sunburst.html b/dammit/viewer/templates/sunburst.html new file mode 100644 index 00000000..98bf01a3 --- /dev/null +++ b/dammit/viewer/templates/sunburst.html @@ -0,0 +1,25 @@ + + +{% include 'js_imports.html' %} +
+ + +
+ diff --git a/dammit/viewer/templates/sunburst.js b/dammit/viewer/templates/sunburst.js new file mode 100644 index 00000000..a269becd --- /dev/null +++ b/dammit/viewer/templates/sunburst.js @@ -0,0 +1,72 @@ +var width = 960, + height = 700, + radius = Math.min(width, height) / 2, + color = d3.scale.category20c(); + +var svg = d3.select("body").append("svg") + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", "translate(" + width / 2 + "," + height * .52 + ")"); + +var partition = d3.layout.partition() + .sort(null) + .size([2 * Math.PI, radius * radius]) + .value(function(d) { return 1; }); + +var arc = d3.svg.arc() + .startAngle(function(d) { return d.x; }) + .endAngle(function(d) { return d.x + d.dx; }) + .innerRadius(function(d) { return Math.sqrt(d.y); }) + .outerRadius(function(d) { return Math.sqrt(d.y + d.dy); }); + +var parser = require("biojs-io-newick"); + +//d3.json("{{ url_for('static', filename='flare.json') }}", function(error, root) { +d3.text("{{ url_for('static', filename='itis.nwk') }}", function(error, nwk) { + if (error) throw error; + + root = parser.parse_newick(nwk); + if (error) throw error; + + var path = svg.datum(root).selectAll("path") + .data(partition.nodes) + .enter().append("path") + .attr("display", function(d) { return d.depth ? null : "none"; }) // hide inner ring + .attr("d", arc) + .style("stroke", "#fff") + .style("fill", function(d) { return color((d.children ? d : d.parent).name); }) + .style("fill-rule", "evenodd") + .each(stash); + + d3.selectAll("input").on("change", function change() { + var value = this.value === "count" + ? function() { return 1; } + : function(d) { return d.size; }; + + path + .data(partition.value(value).nodes) + .transition() + .duration(1500) + .attrTween("d", arcTween); + }); +}); + +// Stash the old values for transition. +function stash(d) { + d.x0 = d.x; + d.dx0 = d.dx; +} + +// Interpolate the arcs in data space. +function arcTween(a) { + var i = d3.interpolate({x: a.x0, dx: a.dx0}, a); + return function(t) { + var b = i(t); + a.x0 = b.x; + a.dx0 = b.dx; + return arc(b); + }; +} + +d3.select(self.frameElement).style("height", height + "px"); diff --git a/dammit/viewer/templates/transcript_model.html b/dammit/viewer/templates/transcript_model.html new file mode 100644 index 00000000..2f6eedec --- /dev/null +++ b/dammit/viewer/templates/transcript_model.html @@ -0,0 +1,114 @@ +
+ +
+
+ + + + + + + + + + +
+
+
diff --git a/dammit/viewer/transcript_pane.py b/dammit/viewer/transcript_pane.py new file mode 100644 index 00000000..7b025efb --- /dev/null +++ b/dammit/viewer/transcript_pane.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +from __future__ import print_function + +import json +import logging +import os + +from flask import Flask, Blueprint, current_app, abort, jsonify +from flask import render_template, redirect, url_for +from jinja2 import TemplateNotFound +import pandas as pd + +from .. import common +from .. import parsers +from . import genomed3plot as gd3 +from .database import db + +from . import static_folder, template_folder + +views = Blueprint('transcript_pane', __name__, + static_folder=static_folder, + template_folder=template_folder) + +@views.errorhandler(404) +def error_404_page(error): + return render_template('404.html', msg=error.description), 404 + +@views.route('/tracks/') +def tracks(transcript): + try: + transcript_info = db['transcripts'][transcript] + except KeyError: + abort(404, 'No transcript named {0} in the annotation database :('.format(transcript)) + else: + tracks = gd3.create_tracks(transcript, transcript_info['annotations']) + length = transcript_info['length'] + result = {'data': {'tracks': tracks, 'length': length}} + return jsonify(result) + +@views.route('/transcripts') +def transcript_list(): + return jsonify({'transcripts': db['annotations'].keys()}) + +@views.route('/transcript/') +def transcript(transcript): + print (transcript) + return render_template('index.html', + name=current_app.config['DIRECTORY'], + transcript=transcript) diff --git a/setup.py b/setup.py index c8a6cc09..ccafb85b 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ def main(): license = 'BSD', test_suite = 'nose.collector', tests_require = ['nose'], - packages = ['dammit'], + packages = find_packages(), scripts = glob('bin/*'), install_requires = ['setuptools>=0.6.35', 'pandas>=0.17', @@ -49,6 +49,6 @@ def main(): 'numexpr>=2.3.1'], include_package_data = True, zip_safe = False, ) - + if __name__ == "__main__": main()