├── version.txt ├── Scribl.min.js ├── examples ├── demos.css ├── benchmark.html ├── scale.html ├── mouseEvents.html ├── genbank.html ├── simple.html ├── viewer.html ├── tracks.html ├── dragNdrop.html ├── complex.html ├── exportSvg.html ├── colors.html ├── js │ └── dragscrollable.js ├── simpleJavascriptIncludes.html ├── builder.html └── bam.html ├── utils ├── compile.rb └── generate-source.rb ├── src ├── parsers │ ├── bed.js │ └── genbank.js ├── glyph │ ├── Scribl.line.js │ ├── Scribl.complex.js │ ├── Scribl.rect.js │ ├── Scribl.arrow.js │ ├── Scribl.blockarrow.js │ └── Scribl.seq.js ├── Scribl.class.js ├── Scribl.utils.js ├── Scribl.lane.js ├── Scribl.events.js ├── Scribl.svg.js ├── Scribl.tooltips.js ├── Scribl.track.js ├── Scribl.glyph.js └── Scribl.js ├── README.mkdn └── lib ├── bin.js └── bam.js /version.txt: -------------------------------------------------------------------------------- 1 | 1.1.5 2 | -------------------------------------------------------------------------------- /Scribl.min.js: -------------------------------------------------------------------------------- 1 | Scribl.1.1.4.min.js -------------------------------------------------------------------------------- /examples/demos.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #C3D3E4; 3 | } 4 | 5 | #description { 6 | margin-left:auto; 7 | margin-right:auto; 8 | width:700px; 9 | font: 20px arial; 10 | color: rgb(64, 64, 64); 11 | text-align: center; 12 | } 13 | 14 | #container { 15 | text-align: center; 16 | } 17 | 18 | .slider { 19 | margin-left: 10px; 20 | font-size: 8px; 21 | width: 100px; 22 | display: inline-block; 23 | } -------------------------------------------------------------------------------- /utils/compile.rb: -------------------------------------------------------------------------------- 1 | closure_path= "~/Tools/google-closure/compiler.jar" 2 | 3 | version = `cat version.txt` 4 | output_filename = 'Scribl.' + version.chomp + '.min.js' 5 | 6 | `java -jar #{closure_path} --js src/Scribl.class.js --js src/Scribl.js --js src/Scribl.track.js --js src/Scribl.lane.js --js src/Scribl.tooltips.js --js src/Scribl.events.js --js src/Scribl.utils.js --js src/Scribl.svg.js --js src/Scribl.glyph.js --js src/glyph/Scribl.blockarrow.js --js src/glyph/Scribl.rect.js --js src/glyph/Scribl.seq.js --js src/glyph/Scribl.line.js --js src/glyph/Scribl.complex.js --js src/glyph/Scribl.arrow.js --js src/parsers/genbank.js --js src/parsers/bed.js --js_output_file #{output_filename}` 7 | 8 | `ln -s #{output_filename} Scribl.min.js` 9 | -------------------------------------------------------------------------------- /utils/generate-source.rb: -------------------------------------------------------------------------------- 1 | # generate html 2 | Dir.chdir('../src') 3 | `dox --ribbon 'https://github.com/chmille4/Scribl' --title Scribl --desc "HTML5 Canvas Genomics Graphics Library" Scribl.js Scribl.track.js Scribl.lane.js Scribl.glyph.js Scribl.events.js Scribl.tooltips.js Scribl.utils.js Scribl.svg.js glyph/Scribl.arrow.js glyph/Scribl.blockarrow.js glyph/Scribl.seq.js glyph/Scribl.complex.js glyph/Scribl.line.js glyph/Scribl.rect.js parsers/bed.js parsers/genbank.js > ../utils/presource.html` 4 | Dir.chdir('../utils') 5 | 6 | # fix issue with special symbols 7 | presource = File.new('presource.html', 'r') 8 | source = File.new('source.html', 'w') 9 | 10 | #symbol_lookup = { "gt" => '>', 'lt' => '<', 'amp' => '&'} 11 | 12 | presource.each_line do |line| 13 | line.gsub!(/&(\S+)<\/span>/) do |match| 14 | "&" + $1 15 | end 16 | source.puts line 17 | end 18 | 19 | # delete presouce 20 | File.delete(presource) -------------------------------------------------------------------------------- /examples/benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 | 23 | 24 |
25 |

Simple Demo


26 | get up and running with minimal code 27 |

28 |
29 | 30 |
Export as PNG (right click Save As) 31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/scale.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 35 | 36 | 37 | 38 | 39 |

Custom Scale

40 | You can force a specific region for your scale. Here an extra 3k bases are shown on either side of the genes 41 |

42 |
43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/mouseEvents.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 33 | 34 | 35 | 36 | 37 |

Events

38 | add click and mouseover events to genes 39 |

40 |
41 | 42 |
43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/parsers/bed.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bed parser 3 | */ 4 | function bed(file, chart) { 5 | try { 6 | var lines = file.split("\n"); 7 | var features = []; 8 | var max = undefined; 9 | var min = undefined; 10 | 11 | // track name=pairedReads description="Clone Paired Reads" useScore=1 12 | var trackInfo = lines[0]; 13 | 14 | // parse genes 15 | numFeatures = lines.length 16 | for( var j=1; j < numFeatures; j++ ) { 17 | if( lines[j] == "" ) break; 18 | 19 | var fields = lines[j].split(/\s+/); 20 | 21 | //chrom chromStart chromEnd name score strand thickStart thickEnd itemRgb blockCount blockSizes blockStarts 22 | var chromStart = parseInt(fields[1]); 23 | var chromEnd = parseInt(fields[2]); 24 | var name = fields[0] + ": " + fields[3]; 25 | var orientation = fields[5]; 26 | var itemRgb = fields[8]; 27 | var blockLengths = fields[10].split(','); 28 | var blockStarts = fields[11].split(','); 29 | 30 | var complex = chart.addFeature( new Complex('complex', chromStart, chromEnd, orientation, [], {'color':itemRgb, 'name':name}) ); 31 | 32 | for( var k=0; k 11 | 12 | 13 | 14 | 15 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ## Examples 43 | http://chmille4.github.com/Scribl/ 44 | 45 | ## Documentation 46 | * [Usage Guide](https://github.com/chmille4/Scribl/wiki) 47 | * [Annotated Source Code](http://chmille4.github.com/Scribl/source.html) 48 | 49 | ## Minification 50 | The [closure compiler](http://code.google.com/closure/compiler/) is used to combine and minify multiple javascript files. To compile the library yourself install google closure compiler, set the correct path for the closure compiler in compile.sh and then run compile.rb from the main directory 51 | 52 | ```ruby utils/compile.rb``` 53 | ## License 54 | MIT License 55 | -------------------------------------------------------------------------------- /src/parsers/genbank.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Genbank parser 3 | */ 4 | function genbank(file, bchart) { 5 | try { 6 | var lines = file.split("\n"); 7 | var re = new RegExp(/\s+gene\s+([a-z]*)\(?(\d+)\.\.(\d+)/); 8 | var genes = []; 9 | var max = undefined; 10 | var min = undefined; 11 | 12 | // parse genes 13 | for( var j=0; j < lines.length; j++ ) { 14 | 15 | var gene_info; 16 | if (gene_info = lines[j].match(re)) { 17 | gene_info.shift(); 18 | genes.push(gene_info); 19 | 20 | // determine scale dimensions 21 | var end = gene_info[2]; 22 | if (max == undefined || max > end) 23 | max = end; 24 | var position = gene_info[1]; 25 | if (min == undefined || min < position) 26 | min = position; 27 | } 28 | } 29 | 30 | 31 | // set scale dimensions 32 | bchart.scale.max = max; 33 | bchart.scale.min = min; 34 | 35 | // add genes to chart 36 | for(var i=0; i < genes.length; i++ ) { 37 | 38 | // get positional values 39 | var strand = '+'; 40 | if ( genes[i][0] == "complement" ) 41 | strand = '-'; 42 | 43 | var position = genes[i][1]; 44 | var end = genes[i][2]; 45 | position = position - 1 + 1; // force to be integer - TODO make bChart catch non-ints automatically and gracefully fail 46 | var length = end - position; 47 | 48 | bchart.addGene(position, length, strand); 49 | 50 | } 51 | } 52 | catch(err) { 53 | throw('Parsing Error: could not parse genbank file'); 54 | } 55 | } -------------------------------------------------------------------------------- /examples/genbank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 36 | 37 | 38 | 39 | 40 |

Visualize Genbank Files

41 | displaying genbank file of 360kb region of the ecoli genome (AC: FR719195) 42 |
43 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 38 | 39 | 40 | 41 | 42 |
43 |

Simple Demo


44 | get up and running with minimal code 45 |

46 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scribl - viewer 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 38 | 39 | 40 | 41 | 42 |

Simple Viewer

43 |
(Best viewed in Chrome/Safari)
44 |
45 |
46 |
Viewing 360kb region of Ecoli Genome

47 |
Zoom with vertical slider
Drag to scroll left and right
48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /src/glyph/Scribl.line.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Glyph::Line** 3 | * 4 | * _Glyph used to draw any line shape_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | var Line = Glyph.extend({ 10 | /** **init** 11 | 12 | * _Constructor, call this with `new Line()`_ 13 | 14 | * @param {String} type - a tag to associate this glyph with 15 | * @param {Int} position - start position of the glyph 16 | * @param {Int} length - length of the glyph 17 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 18 | * @api public 19 | */ 20 | init: function(type, position, length, opts) { 21 | this.thickness = 2; 22 | this._super(type, position, length, undefined, opts); 23 | this.glyphType = "Line"; 24 | }, 25 | 26 | /** **_draw** 27 | 28 | * _private line specific draw method that gets called by this._super.draw()_ 29 | 30 | * @param [context] - optional canvas.context 31 | * @param [length] - optional length of glyph/feature 32 | * @param [height] - optional height of lane 33 | * @param [roundness] - optional roundness of glyph/feature 34 | * @api internal 35 | */ 36 | _draw: function(ctx, length, height, roundness) { 37 | 38 | // initialize 39 | var line = this; 40 | 41 | // see if optional parameters 42 | var ctx = ctx || line.ctx; 43 | var length = length || line.getPixelLength(); 44 | var height = height || line.getHeight(); 45 | 46 | // Set starting draw position 47 | x = y = 0; 48 | 49 | ctx.beginPath(); 50 | ctx.moveTo(x, height/2 - line.thickness/2); 51 | ctx.lineTo(x, height/2 + line.thickness/2); 52 | ctx.lineTo(x+length, height/2 + line.thickness/2); 53 | ctx.lineTo(x+length, height/2 - line.thickness/2); 54 | // ctx.fill(); 55 | // ctx.fillRect(x, height/2 - line.thickness/2, length, line.thickness); 56 | } 57 | }); -------------------------------------------------------------------------------- /src/Scribl.class.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* Simple JavaScript Inheritance 4 | 5 | * By John Resig http://ejohn.org/ 6 | 7 | * MIT Licensed. 8 | */ 9 | // Inspired by base2 and Prototype 10 | 11 | (function(){ 12 | var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 13 | // The base Class implementation (does nothing) 14 | this.Class = function(){}; 15 | 16 | // Create a new Class that inherits from this class 17 | Class.extend = function(prop) { 18 | var _super = this.prototype; 19 | 20 | // Instantiate a base class (but only create the instance, 21 | // don't run the init constructor) 22 | initializing = true; 23 | var prototype = new this(); 24 | initializing = false; 25 | 26 | // Copy the properties over onto the new prototype 27 | for (var name in prop) { 28 | // Check if we're overwriting an existing function 29 | prototype[name] = typeof prop[name] == "function" && 30 | typeof _super[name] == "function" && fnTest.test(prop[name]) ? 31 | (function(name, fn){ 32 | return function() { 33 | var tmp = this._super; 34 | 35 | // Add a new ._super() method that is the same method 36 | // but on the super-class 37 | this._super = _super[name]; 38 | 39 | // The method only need to be bound temporarily, so we 40 | // remove it when we're done executing 41 | var ret = fn.apply(this, arguments); 42 | this._super = tmp; 43 | 44 | return ret; 45 | }; 46 | })(name, prop[name]) : 47 | prop[name]; 48 | } 49 | 50 | // The dummy class constructor 51 | 52 | function Class() { 53 | // All construction is actually done in the init method 54 | if ( !initializing && this.init ) 55 | this.init.apply(this, arguments); 56 | } 57 | 58 | // Populate our constructed prototype object 59 | Class.prototype = prototype; 60 | 61 | // Enforce the constructor to be what we expect 62 | Class.constructor = Class; 63 | 64 | // And make this class extendable 65 | Class.extend = arguments.callee; 66 | 67 | return Class; 68 | }; 69 | })(); -------------------------------------------------------------------------------- /examples/tracks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 60 | 61 | 62 | 63 | 64 |

Custom Tracks

65 | finer grain control of features placement is possible by adding genes at the track or lane level 66 |

67 |
68 | 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /examples/dragNdrop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Drag and Drop

14 | 15 |
16 |

17 |
18 |
19 | 20 |
21 | 22 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/Scribl.utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Utils** 3 | * 4 | * Chase Miller 2011 5 | */ 6 | 7 | /** **ScriblWrapLines** 8 | 9 | * _transforms text to fit in a column of given width_ 10 | 11 | * @param {Int} max - column width in letters 12 | * @param {String} text 13 | * @return {String} formatted text 14 | * @api internal 15 | */ 16 | function ScriblWrapLines(max, text) { 17 | var lines = []; 18 | text = "" + text; 19 | var temp = ""; 20 | var chcount = 0; 21 | var linecount = 0; 22 | var words = text.split(" "); 23 | 24 | for (var i=0; i < words.length; i++) { 25 | if ((words[i].length + temp.length) <= max) 26 | temp += " " + words[i] 27 | else { 28 | // word is bigger than line break 29 | if (temp == "") { 30 | trunc1 = words[i].slice(0, max-1); 31 | temp += " " + trunc1 + "-" 32 | trunc2 = words[i].slice(max, words[i].length); 33 | words.splice(i+1, 0, trunc2); 34 | lines.push(temp); 35 | temp = ""; 36 | linecount++; 37 | } 38 | else { 39 | i--; 40 | lines.push(temp); 41 | linecount++; 42 | temp = ""; 43 | } 44 | } 45 | } 46 | linecount++; 47 | lines.push(temp) 48 | return ([lines, linecount]); // sends value of temp back 49 | } 50 | 51 | 52 | /** create unique ids */ 53 | var idCounter = 0; 54 | _uniqueId = function(prefix) { 55 | var id = idCounter++; 56 | return prefix ? prefix + id : id; 57 | }; 58 | 59 | // polyfill for older browsers 60 | Object.keys=Object.keys||function(o,k,r){r=[];for(k in o)r.hasOwnProperty.call(o,k)&&r.push(k);return r} 61 | 62 | 63 | /** add indexOf if not implemented for compatibility 64 | with older browsers 65 | */ 66 | if (!Array.prototype.indexOf) { 67 | Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { 68 | "use strict"; 69 | if (this === void 0 || this === null) { 70 | throw new TypeError(); 71 | } 72 | var t = Object(this); 73 | var len = t.length >>> 0; 74 | if (len === 0) { 75 | return -1; 76 | } 77 | var n = 0; 78 | if (arguments.length > 0) { 79 | n = Number(arguments[1]); 80 | if (n !== n) { // shortcut for verifying if it's NaN 81 | n = 0; 82 | } else if (n !== 0 && n !== Infinity && n !== -Infinity) { 83 | n = (n > 0 || -1) * Math.floor(Math.abs(n)); 84 | } 85 | } 86 | if (n >= len) { 87 | return -1; 88 | } 89 | var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); 90 | for (; k < len; k++) { 91 | if (k in t && t[k] === searchElement) { 92 | return k; 93 | } 94 | } 95 | return -1; 96 | } 97 | } -------------------------------------------------------------------------------- /examples/complex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 74 | 75 | 76 | 77 | 78 |
79 |

Complex Glyphs


80 | combine multiple glyphs to act as one 81 |

82 |
83 | 84 |
85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /examples/exportSvg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 58 | 59 | 60 | 61 | 62 |
63 |

Simple Demo


64 | get up and running with minimal code 65 |

66 |
67 | 68 |
Export as PNG (right click Save As) 69 |
Export as SVG 70 |
72 |
73 | 74 |
75 | 76 | 77 |
78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /examples/colors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 75 | 76 | 77 | 78 | 79 | 80 |

Versatility

81 |
Easily change:
background color, scale size, major tick color, minor tick color, scale text color, scale font size, gene roundness 82 |
83 | 84 |

85 | 86 |
87 | 89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/glyph/Scribl.complex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Glyph::Complex** 3 | * 4 | * _Complex is used to draw any feature that has splices 5 | * (e.g gene with subFeatures and introns, etc) Or 6 | * any feature that should be made up of other features_ 7 | * 8 | * Chase Miller 2011 9 | */ 10 | 11 | var Complex = Glyph.extend({ 12 | /** **init** 13 | 14 | * _Constructor, call this with `new Complex()`_ 15 | 16 | * @param {String} type - a tag to associate this glyph with 17 | * @param {Int} position - start position of the glyph 18 | * @param {Int} length - length of the glyph 19 | * @param {Array} subFeatures - array of derived Glyph objects (e.g Rect, Arrow, etc...) 20 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 21 | * @api public 22 | */ 23 | init: function(type, position, length, strand, subFeatures, opts) { 24 | // call super init method to initialize glyph 25 | this._super(type, position, length, strand, opts); 26 | 27 | // instantiate and set defaults 28 | this.slope = 1; 29 | this.glyphType = "Complex"; 30 | this.subFeatures = subFeatures; 31 | 32 | // instantiate connector line and set default attributes 33 | this.line = new Line(type, 0, length); 34 | this.line.parent = this; 35 | this.line.color = "black"; 36 | this.line.thickness = 2; 37 | }, 38 | 39 | /** **addSubFeature** 40 | 41 | * _adds subFeature to complex glyph/feature_ 42 | 43 | * @param subFeature - a derived Glyph object (e.g. Rect, Arrow, etc..) 44 | * @api public 45 | */ 46 | addSubFeature : function(subFeature) { 47 | this.subFeatures.push(subFeature); 48 | }, 49 | 50 | /** **_draw** 51 | 52 | * _private complex specific draw method that gets called by this._super.draw()_ 53 | 54 | * @param [context] - optional canvas.context 55 | * @param [length] - optional length of glyph/feature 56 | * @param [height] - optional height of lane 57 | * @param [roundness] - optional roundness of glyph/feature 58 | * @api internal 59 | */ 60 | _draw : function(ctx, length, height, roundness) { 61 | 62 | // Initialize 63 | var complex = this; 64 | 65 | 66 | // see if optional parameters are set and get chart specific info 67 | var ctx = ctx || complex.ctx; 68 | var length = length || complex.getPixelLength(); 69 | var height = height || complex.getHeight(); 70 | var roundness = roundness + 1 || complex.calcRoundness(); 71 | if (roundness != undefined) roundness -= 1; 72 | 73 | // set start x and y draw locations to 0 74 | x = y = 0; 75 | 76 | // translate back the length of the complex glyph 77 | // so sub glyphs will be placed correctly 78 | ctx.translate(-complex.getPixelPositionX(), 0); 79 | 80 | // draw connector line 81 | complex.line.lane = this.lane; 82 | complex.line.draw(); 83 | 84 | // draw subFeatures 85 | var numsubFeatures = complex.subFeatures.length 86 | for (var i=0; i< numsubFeatures; i++) { 87 | // set subFeature to same lane and draw 88 | complex.subFeatures[i].parent = complex; 89 | complex.subFeatures[i].lane = complex.lane; 90 | complex.subFeatures[i].draw(); 91 | } 92 | 93 | // redo translate so the next glyphs will be placed correctly 94 | ctx.translate(complex.getPixelPositionX(), 0); 95 | 96 | // end path so it doesn't get redrawn when parent tries to draw 97 | ctx.beginPath(); 98 | } 99 | 100 | }); 101 | 102 | 103 | -------------------------------------------------------------------------------- /lib/bin.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: javascript; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | 3 | // 4 | // Dalliance Genome Explorer 5 | // (c) Thomas Down 2006-2011 6 | // 7 | // bin.js general binary data support 8 | // 9 | 10 | function BlobFetchable(b) { 11 | this.blob = b; 12 | } 13 | 14 | BlobFetchable.prototype.slice = function(start, length) { 15 | var b; 16 | 17 | if (length) { 18 | if (this.blob.webkitSlice) 19 | b = this.blob.webkitSlice(start, start + length); 20 | else if (this.blob.mozSlice) 21 | b = this.blob.mozSlice(start, start + length); 22 | } else { 23 | if (this.blob.webkitSlice) 24 | b = this.blob.webkitSlice(start); 25 | else if (this.blob.mozSlice) 26 | b = this.blob.mozSlice(start); 27 | } 28 | return new BlobFetchable(b); 29 | } 30 | 31 | BlobFetchable.prototype.fetch = function(callback) { 32 | var reader = new FileReader(); 33 | reader.onloadend = function(ev) { 34 | callback(bstringToBuffer(reader.result)); 35 | }; 36 | reader.readAsBinaryString(this.blob); 37 | 38 | } 39 | 40 | 41 | function URLFetchable(url, start, end, opts) { 42 | if (!opts) { 43 | if (typeof start === 'object') { 44 | opts = start; 45 | start = undefined; 46 | } else { 47 | opts = {}; 48 | } 49 | } 50 | 51 | this.url = url; 52 | this.start = start || 0; 53 | if (end) { 54 | this.end = end; 55 | } 56 | this.opts = opts; 57 | } 58 | 59 | URLFetchable.prototype.slice = function(s, l) { 60 | var ns = this.start, ne = this.end; 61 | if (ns && s) { 62 | ns = ns + s; 63 | } else { 64 | ns = s || ns; 65 | } 66 | if (l && ns) { 67 | ne = ns + l - 1; 68 | } else { 69 | ne = ne || l - 1; 70 | } 71 | return new URLFetchable(this.url, ns, ne, this.opts); 72 | } 73 | 74 | URLFetchable.prototype.fetch = function(callback, attempt, truncatedLength) { 75 | var thisB = this; 76 | 77 | attempt = attempt || 1; 78 | if (attempt > 3) { 79 | return callback(null); 80 | } 81 | 82 | var req = new XMLHttpRequest(); 83 | var length; 84 | req.open('GET', this.url, true); 85 | req.overrideMimeType('text/plain; charset=x-user-defined'); 86 | if (this.end) { 87 | req.setRequestHeader('Range', 'bytes=' + this.start + '-' + this.end); 88 | length = this.end - this.start + 1; 89 | } 90 | req.responseType = 'arraybuffer'; 91 | req.onreadystatechange = function() { 92 | if (req.readyState == 4) { 93 | if (req.status == 200 || req.status == 206) { 94 | if (req.response) { 95 | return callback(req.response); 96 | } else if (req.mozResponseArrayBuffer) { 97 | return callback(req.mozResponseArrayBuffer); 98 | } else { 99 | var r = req.responseText; 100 | if (length && length != r.length && (!truncatedLength || r.length != truncatedLength)) { 101 | return thisB.fetch(callback, attempt + 1, r.length); 102 | } else { 103 | alert(req.responseText); 104 | return callback(bstringToBuffer(req.responseText)); 105 | } 106 | } 107 | } else { 108 | return thisB.fetch(callback, attempt + 1); 109 | } 110 | } 111 | }; 112 | if (this.opts.credentials) { 113 | req.withCredentials = true; 114 | } 115 | req.send(''); 116 | } 117 | 118 | function bstringToBuffer(result) { 119 | if (!result) { 120 | return null; 121 | } 122 | 123 | // var before = Date.now(); 124 | var ba = new Uint8Array(result.length); 125 | for (var i = 0; i < ba.length; ++i) { 126 | ba[i] = result.charCodeAt(i); 127 | } 128 | // var after = Date.now(); 129 | // dlog('bb took ' + (after - before) + 'ms'); 130 | return ba.buffer; 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/glyph/Scribl.rect.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * **Scribl::Glyph::Rect** 4 | * 5 | * _Glyph used to draw any rectangle shape_ 6 | * 7 | * Chase Miller 2011 8 | */ 9 | var Rect = Glyph.extend({ 10 | /** **init** 11 | 12 | * _Constructor, call this with `new Rect()`_ 13 | 14 | * @param {String} type - a tag to associate this glyph with 15 | * @param {Int} position - start position of the glyph 16 | * @param {Int} length - length of the glyph 17 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 18 | * @api public 19 | */ 20 | init: function(type, position, length, opts) { 21 | this._super(type, position, length, undefined, opts); 22 | //this._super(type, position, length, '+', opts); 23 | this.glyphType = "Rect"; 24 | }, 25 | 26 | /** **_draw** 27 | 28 | * _private rect specific draw method that gets called by this._super.draw()_ 29 | 30 | * @param [context] - optional canvas.context 31 | * @param [length] - optional length of glyph/feature 32 | * @param [height] - optional height of lane 33 | * @param [roundness] - optional roundness of glyph/feature 34 | * @api internal 35 | */ 36 | _draw: function(ctx, length, height, roundness) { 37 | 38 | // initialize 39 | var rect = this; 40 | 41 | // see if optional parameters are set 42 | var ctx = ctx || rect.ctx; 43 | var length = length || rect.getPixelLength(); 44 | var height = height || rect.getHeight(); 45 | var roundness = roundness + 1 || rect.calcRoundness(); 46 | if (roundness != undefined) roundness -= 1 47 | 48 | // Set starting draw position 49 | x = y = 0; 50 | 51 | 52 | ctx.beginPath(); 53 | 54 | // calculate points 55 | 56 | // top left corner 57 | tlc_ctrl_x = x; // control point 58 | tlc_ctrl_y = y; 59 | tlc_lgth_x = x + roundness; // horizontal point 60 | tlc_lgth_y = y; 61 | tlc_wdth_x = x; // vertical point 62 | tlc_wdth_y = y + roundness; 63 | 64 | // bottom left corner 65 | blc_ctrl_x = x; // control point 66 | blc_ctrl_y = y + height; 67 | blc_lgth_x = x + roundness; // horizontal point 68 | blc_lgth_y = y + height; 69 | blc_wdth_x = x; // vertical point 70 | blc_wdth_y = y + height - roundness; 71 | 72 | // bottom right corner 73 | brc_ctrl_x = x + length; // control point 74 | brc_ctrl_y = y + height; 75 | brc_lgth_x = x + length - roundness; // horizontal point 76 | brc_lgth_y = y + height; 77 | brc_wdth_x = x + length; // vertical point 78 | brc_wdth_y = y + height - roundness; 79 | 80 | // top right corner 81 | trc_ctrl_x = x + length; // control point 82 | trc_ctrl_y = y; 83 | trc_lgth_x = x + length - roundness; // horizontal point 84 | trc_lgth_y = y; 85 | trc_wdth_x = x + length; // vertical point 86 | trc_wdth_y = y + roundness; 87 | 88 | // draw lines 89 | 90 | // top left corner 91 | ctx.moveTo(tlc_lgth_x, tlc_lgth_y); 92 | ctx.quadraticCurveTo(tlc_ctrl_x, tlc_ctrl_y, tlc_wdth_x, tlc_wdth_y); 93 | 94 | // bottom left corner 95 | ctx.lineTo(blc_wdth_x, blc_wdth_y); 96 | ctx.quadraticCurveTo(blc_ctrl_x, blc_ctrl_y, blc_lgth_x, blc_lgth_y); 97 | 98 | // bottom right corner 99 | ctx.lineTo(brc_lgth_x, brc_lgth_y); 100 | ctx.quadraticCurveTo(brc_ctrl_x, brc_ctrl_y, brc_wdth_x, brc_wdth_y); 101 | 102 | // top right corner 103 | ctx.lineTo(trc_wdth_x, trc_wdth_y); 104 | ctx.quadraticCurveTo(trc_ctrl_x, trc_ctrl_y, trc_lgth_x, trc_lgth_y); 105 | 106 | // top line 107 | ctx.lineTo(tlc_lgth_x, tlc_lgth_y); 108 | } 109 | }); -------------------------------------------------------------------------------- /examples/js/dragscrollable.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery dragscrollable Plugin 3 | * version: 1.0 (25-Jun-2009) 4 | * Copyright (c) 2009 Miquel Herrera 5 | * 6 | * Dual licensed under the MIT and GPL licenses: 7 | * http://www.opensource.org/licenses/mit-license.php 8 | * http://www.gnu.org/licenses/gpl.html 9 | * 10 | */ 11 | ;(function(jQuery){ // secure $ jQuery alias 12 | 13 | /** 14 | * Adds the ability to manage elements scroll by dragging 15 | * one or more of its descendant elements. Options parameter 16 | * allow to specifically select which inner elements will 17 | * respond to the drag events. 18 | * 19 | * options properties: 20 | * ------------------------------------------------------------------------ 21 | * dragSelector | jquery selector to apply to each wrapped element 22 | * | to find which will be the dragging elements. 23 | * | Defaults to '>:first' which is the first child of 24 | * | scrollable element 25 | * ------------------------------------------------------------------------ 26 | * acceptPropagatedEvent| Will the dragging element accept propagated 27 | * | events? default is yes, a propagated mouse event 28 | * | on a inner element will be accepted and processed. 29 | * | If set to false, only events originated on the 30 | * | draggable elements will be processed. 31 | * ------------------------------------------------------------------------ 32 | * preventDefault | Prevents the event to propagate further effectivey 33 | * | dissabling other default actions. Defaults to true 34 | * ------------------------------------------------------------------------ 35 | * 36 | * usage examples: 37 | * 38 | * To add the scroll by drag to the element id=viewport when dragging its 39 | * first child accepting any propagated events 40 | * $('#viewport').dragscrollable(); 41 | * 42 | * To add the scroll by drag ability to any element div of class viewport 43 | * when dragging its first descendant of class dragMe responding only to 44 | * evcents originated on the '.dragMe' elements. 45 | * $('div.viewport').dragscrollable({dragSelector:'.dragMe:first', 46 | * acceptPropagatedEvent: false}); 47 | * 48 | * Notice that some 'viewports' could be nested within others but events 49 | * would not interfere as acceptPropagatedEvent is set to false. 50 | * 51 | */ 52 | jQuery.fn.dragscrollable = function( options ){ 53 | 54 | var settings = jQuery.extend( 55 | { 56 | dragSelector:'>:first', 57 | acceptPropagatedEvent: true, 58 | preventDefault: true 59 | },options || {}); 60 | 61 | 62 | var dragscroll= { 63 | mouseDownHandler : function(event) { 64 | // mousedown, left click, check propagation 65 | if (event.which!=1 || 66 | (!event.data.acceptPropagatedEvent && event.target != this)){ 67 | return false; 68 | } 69 | 70 | // Initial coordinates will be the last when dragging 71 | event.data.lastCoord = {left: event.clientX, top: event.clientY}; 72 | 73 | jQuery.event.add( document, "mouseup", 74 | dragscroll.mouseUpHandler, event.data ); 75 | jQuery.event.add( document, "mousemove", 76 | dragscroll.mouseMoveHandler, event.data ); 77 | if (event.data.preventDefault) { 78 | event.preventDefault(); 79 | return false; 80 | } 81 | }, 82 | mouseMoveHandler : function(event) { // User is dragging 83 | // How much did the mouse move? 84 | var delta = {left: (event.clientX - event.data.lastCoord.left), 85 | top: (event.clientY - event.data.lastCoord.top)}; 86 | 87 | // Set the scroll position relative to what ever the scroll is now 88 | event.data.scrollable.scrollLeft( 89 | event.data.scrollable.scrollLeft() - delta.left); 90 | event.data.scrollable.scrollTop( 91 | event.data.scrollable.scrollTop() - delta.top); 92 | 93 | // Save where the cursor is 94 | event.data.lastCoord={left: event.clientX, top: event.clientY} 95 | if (event.data.preventDefault) { 96 | event.preventDefault(); 97 | return false; 98 | } 99 | 100 | }, 101 | mouseUpHandler : function(event) { // Stop scrolling 102 | jQuery.event.remove( document, "mousemove", dragscroll.mouseMoveHandler); 103 | jQuery.event.remove( document, "mouseup", dragscroll.mouseUpHandler); 104 | if (event.data.preventDefault) { 105 | event.preventDefault(); 106 | return false; 107 | } 108 | } 109 | } 110 | 111 | // set up the initial events 112 | this.each(function() { 113 | // closure object data for each scrollable element 114 | var data = {scrollable : jQuery(this), 115 | acceptPropagatedEvent : settings.acceptPropagatedEvent, 116 | preventDefault : settings.preventDefault } 117 | // Set mouse initiating event on the desired descendant 118 | jQuery(this).find(settings.dragSelector). 119 | bind('mousedown', data, dragscroll.mouseDownHandler); 120 | }); 121 | }; //end plugin dragscrollable 122 | 123 | })( jQuery ); // confine scope 124 | -------------------------------------------------------------------------------- /examples/simpleJavascriptIncludes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 109 | 110 | 111 | 112 | 113 |
114 |

Simple Demo


115 | get up and running with minimal code 116 |

117 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /src/glyph/Scribl.arrow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Glyph::Arrow** 3 | * 4 | * _Glyph used to draw any arrow shape_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | 10 | var Arrow = Glyph.extend({ 11 | /** **init** 12 | 13 | * _Constructor, call this with `new Arrow()`_ 14 | 15 | * @param {String} type - a tag to associate this glyph with 16 | * @param {Int} position - start position of the glyph 17 | * @param {Int} length - length of the glyph 18 | * @param {String} strand - '+' or '-' strand 19 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 20 | * @api public 21 | */ 22 | init: function(type, position, strand, opts) { 23 | // call base class glyph init method to initialize glyph 24 | this._super(type, position, 0, strand, opts); 25 | 26 | // set defaults 27 | this.slope = 1; 28 | this.glyphType = "Arrow"; 29 | this.thickness = 4.6 30 | }, 31 | 32 | /** **getPixelThickness** 33 | 34 | * _gets pixel thickness_ 35 | 36 | * @preturn {Int} pixelThickness 37 | * @api internal 38 | */ 39 | getPixelThickness: function() { 40 | var arrow = this; 41 | var height = arrow.getHeight(); 42 | var arrowLength = height/2 / Math.tan(Math.atan(arrow.slope)) 43 | return ( arrow.thickness / 10 * arrowLength ); 44 | }, 45 | 46 | /** **erase** 47 | 48 | * _erase this glyph/feature_ 49 | 50 | * @api public 51 | */ 52 | erase: function() { 53 | var arrow = this; 54 | var thickness = arrow.getPixelThickness(); 55 | arrow.ctx.clearRect( -thickness,0, thickness, arrow.getHeight() ); 56 | }, 57 | 58 | /** **_draw** 59 | 60 | * _private arrow specific draw method that gets called by this._super.draw()_ 61 | 62 | * @param [context] - optional canvas.context 63 | * @param [length] - optional length of glyph/feature 64 | * @param [height] - optional height of lane 65 | * @param [roundness] - optional roundness of glyph/feature 66 | * @api internal 67 | */ 68 | _draw : function(ctx, length, height, roundness) { 69 | 70 | // Initialize 71 | var arrow = this; 72 | 73 | // see if optional parameters are set and get chart specific info 74 | var ctx = ctx || arrow.ctx; 75 | var height = height || arrow.getHeight(); 76 | var roundness = roundness + 1 || arrow.calcRoundness(); 77 | if (roundness != undefined) roundness -= 1; 78 | var thickness = arrow.getPixelThickness(); 79 | var arrowLength = 0; 80 | 81 | // set start x and y draw locations to 0 82 | x = y = 0; 83 | 84 | // arrow x and control coords 85 | a_b_x = x - arrowLength - roundness; // bottom x coord 86 | a_t_x = x - arrowLength - roundness; // top point x coord 87 | a_max_x = x - arrowLength; // the furthest point of the arrow 88 | 89 | // use bezier quadratic equation to calculate control point x coord 90 | t = .5 // solve for end of arrow 91 | a_ctrl_x = ( a_max_x - (1-t)*(1-t)*a_b_x - t*t*a_t_x ) / ( 2*(1-t)*t ) 92 | a_ctrl_y = y + height/2; 93 | 94 | // arrow slope and intercept 95 | bs_slope = arrow.slope; 96 | bs_intercept = (-a_ctrl_y) - bs_slope * a_ctrl_x; 97 | ts_slope = -arrow.slope; 98 | ts_intercept = (-a_ctrl_y) - ts_slope * a_ctrl_x; 99 | 100 | // arrow y coords 101 | a_b_y = -(bs_slope * a_b_x + bs_intercept); 102 | a_t_y = -(ts_slope * a_t_x + ts_intercept); 103 | 104 | // draw lines 105 | ctx.beginPath(); 106 | 107 | 108 | // bottom slope 109 | bs_ctrl_y = y + height; 110 | bs_ctrl_x = ( (-bs_ctrl_y - bs_intercept)/arrow.slope ); // control point 111 | bs_slpe_x = bs_ctrl_x + roundness + roundness; // slope point 112 | bs_slpe_y = -(bs_slope * bs_slpe_x + bs_intercept); 113 | 114 | ctx.moveTo(bs_slpe_x, bs_slpe_y); 115 | 116 | // bottom outer-line 117 | ctx.lineTo( a_b_x, a_b_y ); 118 | 119 | // front part of arrow 120 | ctx.quadraticCurveTo(a_ctrl_x, a_ctrl_y, a_t_x, a_t_y); 121 | 122 | // top outer-line 123 | // top slope 124 | ts_ctrl_y = y; 125 | ts_ctrl_x = (ts_ctrl_y + ts_intercept)/arrow.slope ; // control point 126 | ts_slpe_x = ts_ctrl_x + roundness + roundness; // slope point 127 | ts_slpe_y = -(ts_slope * ts_slpe_x + ts_intercept); 128 | ctx.lineTo(ts_slpe_x, ts_slpe_y); 129 | 130 | 131 | // top u-turn 132 | // angle needed to get the x, y position of a point on the inner line perpendicular to a point on the outer line 133 | var theta = ( Math.PI - Math.abs(Math.atan(arrow.slope)) ) - Math.PI/2; 134 | var dX = Math.sin(theta) * thickness; 135 | var dY = Math.cos(theta) * thickness; 136 | var arcTX = ts_slpe_x - dX; 137 | var arcTY = ts_slpe_y + dY; 138 | ctx.bezierCurveTo(ts_ctrl_x, ts_ctrl_y, ts_ctrl_x-dX, ts_ctrl_y+dY, arcTX, arcTY); 139 | 140 | // inner top-line 141 | ctx.lineTo(a_max_x-thickness, y + height/2); 142 | 143 | 144 | // inner bottom-line 145 | var arcBX = bs_slpe_x - dX; 146 | var arcBY = bs_slpe_y - dY; 147 | ctx.lineTo(arcBX, arcBY); 148 | 149 | // bottom uturn 150 | ctx.bezierCurveTo(bs_ctrl_x-dX, bs_ctrl_y-dY, bs_ctrl_x, bs_ctrl_y, bs_slpe_x, bs_slpe_y); 151 | // ctx.fill(); 152 | } 153 | 154 | }); 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/glyph/Scribl.blockarrow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Glyph::BlockArrow** 3 | * 4 | * _Glyph used to draw any blockarrow shape_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | var BlockArrow = Glyph.extend({ 10 | /** **init** 11 | 12 | * _Constructor, call this with `new BlockArrow()`_ 13 | 14 | * @param {String} type - a tag to associate this glyph with 15 | * @param {Int} position - start position of the glyph 16 | * @param {Int} length - length of the glyph 17 | * @param {String} strand - '+' or '-' strand 18 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 19 | * @api public 20 | */ 21 | init: function(type, position, length, strand, opts) { 22 | // call super init method to initialize glyph 23 | this._super(type, position, length, strand, opts); 24 | this.slope = 1; 25 | this.glyphType = "BlockArrow"; 26 | }, 27 | 28 | /** **_draw** 29 | 30 | * _private blockarrow specific draw method that gets called by this._super.draw()_ 31 | 32 | * @param [context] - optional canvas.context 33 | * @param [length] - optional length of glyph/feature 34 | * @param [height] - optional height of lane 35 | * @param [roundness] - optional roundness of glyph/feature 36 | * @api internal 37 | */ 38 | _draw : function(ctx, length, height, roundness) { 39 | 40 | // Initialize 41 | var blockarrow = this; 42 | 43 | // see if optional parameters are set and get chart specific info 44 | var ctx = ctx || blockarrow.ctx; 45 | var length = length || blockarrow.getPixelLength(); 46 | var height = height || blockarrow.getHeight(); 47 | var roundness = roundness + 1 || blockarrow.calcRoundness(); 48 | if (roundness != undefined) roundness -= 1; 49 | 50 | var side = length*.75; 51 | 52 | 53 | // set start x and y draw locations to 0 54 | x = y = 0; 55 | 56 | // calculate points 57 | 58 | // top corner 59 | tc_ctrl_x = x; // control point 60 | tc_ctrl_y = y; 61 | tc_lgth_x = x + roundness; // horizontal point 62 | tc_lgth_y = y; 63 | tc_wdth_x = x; // vertical point 64 | tc_wdth_y = y + roundness; 65 | 66 | // bottom corner 67 | bc_ctrl_x = x; // control point 68 | bc_ctrl_y = y + height; 69 | bc_lgth_x = x + roundness; // horizontal point 70 | bc_lgth_y = y + height; 71 | bc_wdth_x = x; // vertical point 72 | bc_wdth_y = y + height - roundness; 73 | 74 | // arrow x and control coords 75 | a_b_x = x + length - roundness; // bottom x coord 76 | a_t_x = x + length - roundness; // top point x coord 77 | a_max_x = x + length; // the furthest point of the arrow 78 | // use bezier quadratic equation to calculate control point x coord 79 | t = .5 // solve for end of arrow 80 | a_ctrl_x = Math.round( (( a_max_x - (1-t)*(1-t)*a_b_x - t*t*a_t_x ) / ( 2*(1-t)*t ))*10 )/10; 81 | a_ctrl_y = y + height/2; 82 | 83 | // arrow slope and intercept 84 | bs_slope = blockarrow.slope; 85 | bs_intercept = (-a_ctrl_y) - bs_slope * a_ctrl_x; 86 | ts_slope = -blockarrow.slope; 87 | ts_intercept = (-a_ctrl_y) - ts_slope * a_ctrl_x; 88 | 89 | // arrow y coords 90 | a_b_y = -(Math.round( (bs_slope * a_b_x + bs_intercept)*10 )/10); 91 | a_t_y = -(Math.round( (ts_slope * a_t_x + ts_intercept)*10 )/10); 92 | 93 | 94 | // bottom slope 95 | bs_ctrl_y = y + height; 96 | bs_ctrl_x = ( (-bs_ctrl_y - bs_intercept)/blockarrow.slope ); // control point 97 | if (bs_ctrl_x < x ) { 98 | var r = new Rect(blockarrow.type, 0, length); 99 | r._draw(ctx, length, height, roundness); 100 | return; 101 | } 102 | 103 | bs_lgth_y = y + height; // horizontal point 104 | bs_lgth_x = bs_ctrl_x - roundness; 105 | bs_slpe_x = bs_ctrl_x + roundness; // slope point 106 | bs_slpe_y = -(Math.round( (bs_slope * bs_slpe_x + bs_intercept)*10 )/10); 107 | 108 | // top slope 109 | ts_ctrl_y = y; 110 | ts_ctrl_x = (ts_ctrl_y + ts_intercept)/blockarrow.slope ; // control point 111 | ts_lgth_y = y; // horizontal point 112 | ts_lgth_x = ts_ctrl_x - roundness; 113 | ts_slpe_x = ts_ctrl_x + roundness; // slope point 114 | ts_slpe_y = -(Math.round( (ts_slope * ts_slpe_x + ts_intercept)*10 )/10); 115 | 116 | 117 | // draw lines 118 | ctx.beginPath(); 119 | 120 | // top left corner 121 | ctx.moveTo(tc_lgth_x, tc_lgth_y); 122 | ctx.quadraticCurveTo(tc_ctrl_x, tc_ctrl_y, tc_wdth_x, tc_wdth_y); 123 | 124 | // bottom left corner 125 | ctx.lineTo(bc_wdth_x, bc_wdth_y); 126 | ctx.quadraticCurveTo(bc_ctrl_x, bc_ctrl_y, bc_lgth_x, bc_lgth_y); 127 | 128 | // bottom right slope 129 | ctx.lineTo(bs_lgth_x, bs_lgth_y); 130 | ctx.quadraticCurveTo(bs_ctrl_x, bs_ctrl_y, bs_slpe_x, bs_slpe_y); 131 | 132 | // arrow 133 | ctx.lineTo( a_b_x, a_b_y ); 134 | ctx.quadraticCurveTo(a_ctrl_x, a_ctrl_y, a_t_x, a_t_y); 135 | 136 | // top right slope 137 | ctx.lineTo(ts_slpe_x, ts_slpe_y); 138 | ctx.quadraticCurveTo(ts_ctrl_x, ts_ctrl_y, ts_lgth_x, ts_lgth_y); 139 | 140 | // top line 141 | ctx.lineTo(tc_lgth_x, tc_lgth_y); 142 | } 143 | 144 | }); 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/Scribl.lane.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Lane** 3 | * 4 | * _A lane is used to draw features on a single y position_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | 10 | var Lane = Class.extend({ 11 | /** **init** 12 | 13 | * _Constructor_ 14 | * 15 | * This is called with `new Lane()`, but to create new Lanes associated with a chart use `track.addLane()` 16 | * 17 | * @param {Object} ctx - the canvas.context object 18 | * @param {Object} track - track that this lane belongs to 19 | * @api internal 20 | */ 21 | init: function(ctx, track) { 22 | // defaults 23 | this.height = undefined; 24 | this.features = []; 25 | this.ctx = ctx; 26 | this.track = track; 27 | this.chart = track.chart; 28 | this.uid = _uniqueId('lane'); 29 | }, 30 | 31 | 32 | /** **addGene** 33 | 34 | * _syntactic sugar function to add a feature with the gene type to this Lane_ 35 | 36 | * @param {Int} position - start position of the gene 37 | * @param {Int} length - length of the gene 38 | * @param {String} strand - '+' or '-' strand 39 | * @param {Hash} [opts] - optional hash of options that can be applied to gene 40 | * @return {Object} gene - a feature with the 'gene' type 41 | * @api public 42 | */ 43 | addGene: function(position, length, strand, opts) { 44 | return (this.addFeature( new BlockArrow("gene", position, length, strand, opts) ) ); 45 | }, 46 | 47 | /** **addProtein** 48 | 49 | * _syntactic sugar function to add a feature with the protein type to this Lane_ 50 | 51 | * @param {Int} position - start position of the protein 52 | * @param {Int} length - length of the protein 53 | * @param {String} strand - '+' or '-' strand 54 | * @param {Hash} [opts] - optional hash of options that can be applied to protein 55 | * @return {Object} protein - a feature with the 'protein' type 56 | * @api public 57 | */ 58 | addProtein: function(position, length, strand, opts) { 59 | return (this.addFeature( new BlockArrow("protein", position, length, strand, opts) ) ); 60 | }, 61 | 62 | /** **addFeature** 63 | 64 | * _addFeature to this Lane, allowing potential overlaps_ 65 | 66 | * example: 67 | * `lane.addFeature( new Rect('complex',3500, 2000) );` 68 | 69 | * @param {Object} feature - any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 70 | * @return {Object} feature - new feature 71 | * @api public 72 | */ 73 | addFeature: function( feature ) { 74 | 75 | // create feature 76 | feature.lane = this; 77 | this.features.push(feature); 78 | 79 | // initialize hash containers for "type" level options 80 | if (! this.chart[feature.type] ){ 81 | this.chart[feature.type] = {'text': {}} 82 | } 83 | 84 | // determine chart absolute_min and absolute_max 85 | if ( feature.length + feature.position > this.chart.scale.max || !this.chart.scale.max ) 86 | this.chart.scale.max = feature.length + feature.position; 87 | if ( feature.position < this.chart.scale.min || !this.chart.scale.min ) 88 | this.chart.scale.min = feature.position; 89 | 90 | return feature; 91 | }, 92 | 93 | /** **loadFeatures** 94 | 95 | * _adds the features to this Lane_ 96 | 97 | * @param {Array} features - array of features, which can be any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 98 | * @api public 99 | */ 100 | loadFeatures: function(features) { 101 | var featureNum = features.length; 102 | for(var i=0; i= min && pos <= max || end >= min && end <= max) { 165 | this.features[i].draw(); 166 | hasGlyphs = true; 167 | } 168 | } 169 | return hasGlyphs; 170 | }, 171 | 172 | /** **filterFeaturesByPosition** 173 | 174 | * _returns an array of features that fall inside a given range_ 175 | 176 | * @param {Int} start - the start of the range 177 | * @param {Int} end - the end of the range 178 | * @return {Array} features - the features that any part of which fall inside that range 179 | * @api public 180 | */ 181 | 182 | filterFeaturesByPosition: function(start, end) { 183 | var lane = this; 184 | var features = []; 185 | var numFeatures = lane.features.length; 186 | 187 | for( var i=0; i < numFeatures; i++ ) { 188 | var ftStart = lane.features[i].position; 189 | var ftEnd = lane.features[i].getEnd(); 190 | 191 | if ( (ftStart >= start && ftStart <= end) || (ftEnd >= start && ftEnd <= end) ) 192 | features.push( lane.features[i] ); 193 | } 194 | 195 | return features; 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /examples/builder.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scribl - builder 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 36 | 37 | 114 | 115 | 116 | 117 | 118 |
119 |

Chart Builder

(Best viewed in Chrome/Safari)

120 | since the chart is being drawn in the browser, changes are instant 121 |
122 |
123 |
124 |
125 | Gene Shape
126 |
127 | 128 |
Track Size
129 |
130 | 131 |
132 | 133 | 134 |
135 |
136 | Gene Color 1
137 | r

138 | g

139 | b
140 | 143 |
144 |
145 | Gene Color 2
146 | r

147 | g

148 | b
149 | 152 |
153 |
154 | 155 |

156 |
157 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /src/Scribl.events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Events** 3 | * 4 | * _Adds event support to Scribl_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | var MouseEventHandler = Class.extend({ 10 | /** **init** 11 | 12 | * _Constructor, call this with `new MouseEventHandler()`_ 13 | 14 | * @param {Object} chart - Scribl object 15 | * @return {Object} MouseEventHandler object 16 | * @api internal 17 | */ 18 | init: function(chart) { 19 | this.chart = chart; 20 | this.mouseX = null; 21 | this.mouseY = null; 22 | this.eventElement = undefined; 23 | this.isEventDetected = false; 24 | this.tooltip = new Tooltip("", 'above', -4); 25 | }, 26 | 27 | /** **addEvents** 28 | 29 | * _registers event listeners if feature (or parent if part of complex feature) has mouse events associated with it_ 30 | 31 | * @param {Object} feature - any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 32 | * @api internal 33 | */ 34 | addEvents: function(feature) { 35 | var chart = this.chart; 36 | var ctx = chart.ctx; 37 | var me = chart.myMouseEventHandler; 38 | 39 | // check if any features use onmouseover and if so register an event listener if not already done 40 | if (feature.onMouseover && !chart.events.hasMouseover ) { 41 | chart.addMouseoverEventListener(chart.myMouseEventHandler.handleMouseover); 42 | chart.events.hasMouseover = true; 43 | } 44 | else if (feature.tooltips.length>0 && !chart.events.hasMouseover){ 45 | chart.addMouseoverEventListener(chart.myMouseEventHandler.handleMouseover); 46 | chart.events.hasMouseover = true; 47 | } 48 | else if (feature.parent && feature.parent.tooltips.length>0 && !chart.events.hasMouseover){ 49 | chart.addMouseoverEventListener(chart.myMouseEventHandler.handleMouseover); 50 | chart.events.hasMouseover = true; 51 | } 52 | else if (feature.parent && feature.parent.onMouseover && !chart.events.hasMouseover) { 53 | chart.addMouseoverEventListener(chart.myMouseEventHandler.handleMouseover); 54 | chart.events.hasMouseover = true; 55 | } 56 | 57 | // check if any features use onclick and if so register event listeners if not already done 58 | if (feature.onClick && !chart.events.hasClick) { 59 | chart.addClickEventListener(chart.myMouseEventHandler.handleClick); 60 | chart.addMouseoverEventListener(chart.myMouseEventHandler.handleMouseStyle); 61 | chart.events.hasClick = true; 62 | } else if (feature.parent && feature.parent.onClick && !chart.events.hasClick) { 63 | chart.addClickEventListener(chart.myMouseEventHandler.handleClick); 64 | chart.addMouseoverEventListener(chart.myMouseEventHandler.handleMouseStyle); 65 | chart.events.hasClick = true; 66 | } 67 | 68 | // determine if cursor is currently in a drawn object (feature) 69 | if (!me.isEventDetected && ctx.isPointInPath_mozilla(me.mouseX,me.mouseY)) { 70 | me.eventElement = feature; 71 | me.isEventDetected = true; 72 | } 73 | }, 74 | 75 | /** **setMousePosition** 76 | 77 | * _sets the mouse position relative to the canvas_ 78 | 79 | * @param {Object} e - event 80 | * @api internal 81 | */ 82 | setMousePosition: function(e) { 83 | if (e!=null) { 84 | var rect = this.chart.canvas.getBoundingClientRect(); 85 | this.mouseX = e.clientX - rect.left; 86 | this.mouseY = e.clientY - rect.top; 87 | } 88 | }, 89 | 90 | /** **handleClick** 91 | 92 | * _gets called when there is a click and determines how to handle it_ 93 | 94 | * @param {Object} chart - Scribl object 95 | * @api internal 96 | */ 97 | handleClick: function(chart) { 98 | var me = chart.myMouseEventHandler; 99 | var clicked = me.eventElement; 100 | var onClick; 101 | 102 | // check if the click occured on a feature/object with an onClick property 103 | if (clicked != undefined && clicked.onClick != undefined) 104 | onClick = clicked.onClick 105 | else if (clicked && clicked.parent && clicked.parent.onClick) 106 | onClick = clicked.parent.onClick 107 | 108 | if(onClick){ 109 | // open window if string 110 | if (typeof(onClick) == "string"){ window.open(onClick); } 111 | // if function run function with feature as argument 112 | else if (typeof(onClick) == "function"){ onClick(clicked); } 113 | } 114 | }, 115 | 116 | /** **handleMouseover** 117 | 118 | * _gets called when there is a mouseover and fires tooltip if necessary_ 119 | 120 | * @param {Object} chart - Scribl object 121 | * @api internal 122 | */ 123 | handleMouseover: function(chart) { 124 | var me = chart.myMouseEventHandler; 125 | var clicked = me.eventElement; 126 | 127 | // handle mouseover tooltips 128 | if (clicked && clicked.onMouseover == undefined && clicked.parent && clicked.parent.onMouseover) { 129 | clicked.onMouseover = clicked.parent.onMouseover 130 | } 131 | 132 | if(clicked && clicked.onMouseover) { 133 | // open window if string 134 | if (typeof(clicked.onMouseover) == "string"){ me.tooltip.fire(clicked); } 135 | // if function run function with feature as argument 136 | else if (typeof(clicked.onMouseover) == "function"){ clicked.onMouseover(clicked); } 137 | } 138 | 139 | // handle tooltip object tooltips 140 | if (clicked && clicked.tooltips.length > 0) 141 | clicked.fireTooltips(); 142 | }, 143 | 144 | /** **handleMouseStyle** 145 | 146 | * _changes cursor to pointer if the feature the mouse is over can be clicked_ 147 | 148 | * @param {Object} chart - Scribl object 149 | * @api internal 150 | */ 151 | handleMouseStyle: function(chart) { 152 | var me = chart.myMouseEventHandler; 153 | var obj = me.eventElement; 154 | var ctx = chart.ctx; 155 | 156 | if (obj && obj.onClick != undefined) 157 | ctx.canvas.style.cursor = 'pointer'; 158 | else if (obj && obj.parent && obj.parent.onClick != undefined) 159 | ctx.canvas.style.cursor = 'pointer'; 160 | else 161 | ctx.canvas.style.cursor = 'auto'; 162 | }, 163 | 164 | /** **reset** 165 | 166 | * _resets the state of the mouseEventHandler_ 167 | 168 | * @param {Object} chart - Scribl object 169 | * @api internal 170 | */ 171 | reset: function(chart) { 172 | var me = chart.myMouseEventHandler; 173 | me.mouseX = null; 174 | me.mouseY = null; 175 | me.eventElement = undefined; 176 | me.isEventDetected = null; 177 | me.elementIndexCounter = 0; 178 | } 179 | 180 | }); 181 | 182 | // FIX FOR FIREFOX BUG in ctx.isPointInPath() function 183 | CanvasRenderingContext2D.prototype.isPointInPath_mozilla = function( x, y ) 184 | { 185 | if (navigator.userAgent.indexOf('Firefox') != -1){ 186 | this.save(); 187 | this.setTransform( 1, 0, 0, 1, 0, 0 ); 188 | var ret = this.isPointInPath( x, y ); 189 | this.restore(); 190 | } else 191 | var ret = this.isPointInPath( x, y ); 192 | 193 | return ret; 194 | } 195 | -------------------------------------------------------------------------------- /examples/bam.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 57 | 58 | 59 | 60 |

BAM test

61 |
62 | 63 | Chromosome 64 | Start Pos 65 | End Pos 66 | 67 |
68 | 69 |
70 | 71 |
72 |
73 | 74 | 98 | 99 | 176 | 177 | -------------------------------------------------------------------------------- /src/Scribl.svg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utility functions for converting canvas to svg 3 | */ 4 | 5 | 6 | var CanvasToSVG = { 7 | idCounter: 0, 8 | convert: function(sourceCanvas, targetSVG, x, y) { 9 | var svgNS = "http://www.w3.org/2000/svg"; 10 | var xlinkNS = "http://www.w3.org/1999/xlink"; 11 | 12 | // get base64 encoded png from Canvas 13 | var image = sourceCanvas.toDataURL(); 14 | 15 | // must be careful with the namespaces 16 | var svgimg = document.createElementNS(svgNS, "image"); 17 | 18 | svgimg.setAttribute('id', 'importedCanvas_' + this.idCounter++); 19 | svgimg.setAttributeNS(xlinkNS, 'xlink:href', image); 20 | 21 | svgimg.setAttribute('x', x ? x : 0); 22 | svgimg.setAttribute('y', y ? y : 0); 23 | svgimg.setAttribute('width', sourceCanvas.width); 24 | svgimg.setAttribute('height', sourceCanvas.height); 25 | 26 | // pixel data needs to be saved because of firefox data:// url bug: 27 | // http://markmail.org/message/o2kd3bnnv3vcbwb2 28 | svgimg.imageData = sourceCanvas.toDataURL(); 29 | 30 | targetSVG.appendChild(svgimg); 31 | } 32 | } 33 | 34 | 35 | var toXml = function(str) { 36 | return $('

').text(str).html(); 37 | }; 38 | 39 | // Function: svgCanvasToString 40 | // Main function to set up the SVG content for output 41 | // 42 | // Returns: 43 | // String containing the SVG image for output 44 | 45 | var svgToString = function(svgcontent) { 46 | // keep calling it until there are none to remove 47 | while (removeUnusedDefElems() > 0) {}; 48 | 49 | pathActions.clear(true); 50 | 51 | // Keep SVG-Edit comment on top 52 | $.each(svgcontent.childNodes, function(i, node) { 53 | if(i && node.nodeType == 8 && node.data.indexOf('Created with') >= 0) { 54 | svgcontent.insertBefore(node, svgcontent.firstChild); 55 | } 56 | }); 57 | 58 | // Move out of in-group editing mode 59 | if(current_group) { 60 | leaveContext(); 61 | selectOnly([current_group]); 62 | } 63 | 64 | var naked_svgs = []; 65 | 66 | // Unwrap gsvg if it has no special attributes (only id and style) 67 | $(svgcontent).find('g:data(gsvg)').each(function() { 68 | var attrs = this.attributes; 69 | var len = attrs.length; 70 | for(var i=0; i=0; i--) { 157 | attr = attrs.item(i); 158 | var attrVal = toXml(attr.nodeValue); 159 | //remove bogus attributes added by Gecko 160 | if (['-moz-math-font-style', '_moz-math-font-style'].indexOf(attr.localName) >= 0) continue; 161 | if (attrVal != "") { 162 | if(attrVal.indexOf('pointer-events') === 0) continue; 163 | if(attr.localName === "class" && attrVal.indexOf('se_') === 0) continue; 164 | out.push(" "); 165 | if(attr.localName === 'd') attrVal = pathActions.convertPath(elem, true); 166 | //if(!isNaN(attrVal)) { 167 | // attrVal = shortFloat(attrVal); 168 | //} 169 | 170 | // Embed images when saving 171 | // if(save_options.apply 172 | // && elem.nodeName === 'image' 173 | // && attr.localName === 'href' 174 | // && save_options.images 175 | // && save_options.images === 'embed') 176 | // { 177 | // var img = encodableImages[attrVal]; 178 | // if(img) attrVal = img; 179 | // } 180 | 181 | // map various namespaces to our fixed namespace prefixes 182 | // (the default xmlns attribute itself does not get a prefix) 183 | // if(!attr.namespaceURI || attr.namespaceURI == svgns || nsMap[attr.namespaceURI]) { 184 | out.push(attr.nodeName); out.push("=\""); 185 | out.push(attrVal); out.push("\""); 186 | // } 187 | } 188 | } 189 | } 190 | 191 | if (elem.hasChildNodes()) { 192 | out.push(">"); 193 | indent++; 194 | var bOneLine = false; 195 | 196 | for (var i=0; i"); 217 | break; 218 | } // switch on node type 219 | } 220 | indent--; 221 | if (!bOneLine) { 222 | out.push("\n"); 223 | for (var i=0; i"); 226 | } else { 227 | out.push("/>"); 228 | } 229 | } 230 | return out.join(''); 231 | }; // end svgToString() 232 | 233 | 234 | -------------------------------------------------------------------------------- /src/Scribl.tooltips.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Tooltips** 3 | * 4 | * _Adds event support to Scribl_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | 10 | var Tooltip = Class.extend({ 11 | /** **init** 12 | 13 | * _Constructor, call this with `new tooltips()`_ 14 | 15 | * @param {Object} chart - Scribl object 16 | * @return {Object} tooltip object 17 | * @api internal 18 | */ 19 | init: function(text, placement, verticalOffset, opts) { 20 | var tt = this; 21 | tt.text = text; 22 | tt.placement = placement || 'above'; 23 | tt.verticalOffset = verticalOffset || 0; 24 | // set option attributes if any 25 | for (var attribute in opts) 26 | tt[attribute] = opts[attribute]; 27 | 28 | tt.horizontalOffset = tt.horizontalOffset || 0; 29 | tt.ntOffset = tt.ntOffset || 0; 30 | 31 | }, 32 | 33 | /** **fire** 34 | 35 | * _fires the tooltip_ 36 | 37 | * @param {Object} feature - any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 38 | * @api internal 39 | */ 40 | 41 | fire: function(ft) { 42 | // get curr opacity 43 | var feature = ft || this.feature; 44 | this.chart = feature.lane.track.chart; 45 | this.ctx = this.chart.ctx; 46 | 47 | this.draw(feature, 1); 48 | 49 | }, 50 | 51 | /** **draw** 52 | 53 | * _draws tooltip_ 54 | 55 | * @param {Object} feature - any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 56 | * @param {Int} opacity 57 | * @api internal 58 | */ 59 | draw: function(feature, opacity) { 60 | this.ctx.globalAlpha = opacity; 61 | 62 | // define attributes 63 | var roundness = this.chart.tooltips.roundness; 64 | var font = this.chart.tooltips.text.font; 65 | var fontSize = this.chart.tooltips.text.size; 66 | var text = this.text || feature.onMouseover; 67 | 68 | // Save 69 | this.ctx.save(); 70 | 71 | this.ctx.font = fontSize + "px " + font; 72 | 73 | // determine properties of line 74 | var dim = this.ctx.measureText(text); 75 | var textlines = [text]; 76 | var height = fontSize + 10; 77 | var length = dim.width + 10; 78 | var vertical_offset = height - 4; 79 | var fillStyle; 80 | var strokeStyle; 81 | 82 | // determine nt offset 83 | var ntOffsetPx = 0; 84 | if(feature.seq) { 85 | var lengthPx = feature.getPixelLength(); 86 | ntOffsetPx = this.ntOffset * (lengthPx / feature.length); 87 | } 88 | 89 | // Get coordinates 90 | var x = feature.getPixelPositionX() + this.horizontalOffset + ntOffsetPx; 91 | var y; 92 | if (this.placement == 'below') 93 | y = feature.getPixelPositionY() + feature.getHeight() - this.verticalOffset; 94 | else 95 | y = feature.getPixelPositionY() - height - this.verticalOffset; 96 | 97 | 98 | 99 | 100 | // var x = feature.getPixelPositionX(); 101 | // var y = feature.getPixelPositionY() + feature.getHeight(); 102 | 103 | 104 | // linewrap text 105 | var geneLength = feature.getPixelLength(); 106 | var mincols = 200; 107 | if (length > mincols) { 108 | var charpixel = this.ctx.measureText("s").width; 109 | var max = parseInt(mincols / charpixel); 110 | var formattedText = ScriblWrapLines(max, text); 111 | length = mincols + 10; 112 | height = formattedText[1]*fontSize + 10; 113 | textlines = formattedText[0]; 114 | } 115 | 116 | // check if tooltip will run off screen 117 | if (length + x > this.chart.width) 118 | x = this.chart.width - length; 119 | 120 | // draw light style 121 | if ( this.chart.tooltips.style == "light" ) { 122 | fillStyle = this.chart.ctx.createLinearGradient(x + length/2, y, x + length/2, y + height); 123 | fillStyle.addColorStop(0,'rgb(253, 248, 196)'); 124 | fillStyle.addColorStop(.75,'rgb(253, 248, 196)'); 125 | fillStyle.addColorStop(1,'white'); 126 | 127 | strokeStyle = this.chart.ctx.createLinearGradient(x + length/2, y, x + length/2, y + height); 128 | strokeStyle.addColorStop(0,'black'); 129 | strokeStyle.addColorStop(1,'rgb(64, 64, 64)'); 130 | 131 | this.chart.tooltips.text.color = "black"; 132 | 133 | // draw dark style 134 | } else if ( this.chart.tooltips.style == "dark" ) { 135 | fillStyle = this.chart.ctx.createLinearGradient(x + length/2, y, x + length/2, y + height); 136 | fillStyle.addColorStop(0,'rgb(64, 64, 64)'); 137 | fillStyle.addColorStop(1,'rgb(121, 121, 121)'); 138 | 139 | strokeStyle = "white"; 140 | this.chart.tooltips.text.color = "white"; 141 | } 142 | 143 | this.chart.lastToolTips.push( { 144 | 'pixels' : this.ctx.getImageData(x-1, y-1, length+2, height+2), 145 | 'x' : x-1, 146 | 'y' : y-1 147 | }); 148 | 149 | this.ctx.fillStyle = fillStyle; 150 | 151 | this.ctx.beginPath(); 152 | 153 | // calculate points 154 | 155 | // top left corner 156 | tlc_ctrl_x = x; // control point 157 | tlc_ctrl_y = y; 158 | tlc_lgth_x = x + roundness; // horizontal point 159 | tlc_lgth_y = y; 160 | tlc_wdth_x = x; // vertical point 161 | tlc_wdth_y = y + roundness; 162 | 163 | // bottom left corner 164 | blc_ctrl_x = x; // control point 165 | blc_ctrl_y = y + height; 166 | blc_lgth_x = x + roundness; // horizontal point 167 | blc_lgth_y = y + height; 168 | blc_wdth_x = x; // vertical point 169 | blc_wdth_y = y + height - roundness; 170 | 171 | // bottom right corner 172 | brc_ctrl_x = x + length; // control point 173 | brc_ctrl_y = y + height; 174 | brc_lgth_x = x + length - roundness; // horizontal point 175 | brc_lgth_y = y + height; 176 | brc_wdth_x = x + length; // vertical point 177 | brc_wdth_y = y + height - roundness; 178 | 179 | // top right corner 180 | trc_ctrl_x = x + length; // control point 181 | trc_ctrl_y = y; 182 | trc_lgth_x = x + length - roundness; // horizontal point 183 | trc_lgth_y = y; 184 | trc_wdth_x = x + length; // vertical point 185 | trc_wdth_y = y + roundness; 186 | 187 | 188 | // draw lines 189 | 190 | // top left corner 191 | this.ctx.moveTo(tlc_lgth_x, tlc_lgth_y); 192 | this.ctx.quadraticCurveTo(tlc_ctrl_x, tlc_ctrl_y, tlc_wdth_x, tlc_wdth_y); 193 | 194 | // bottom left corner 195 | this.ctx.lineTo(blc_wdth_x, blc_wdth_y); 196 | this.ctx.quadraticCurveTo(blc_ctrl_x, blc_ctrl_y, blc_lgth_x, blc_lgth_y); 197 | 198 | // bottom right corner 199 | this.ctx.lineTo(brc_lgth_x, brc_lgth_y); 200 | this.ctx.quadraticCurveTo(brc_ctrl_x, brc_ctrl_y, brc_wdth_x, brc_wdth_y); 201 | 202 | // top right corner 203 | this.ctx.lineTo(trc_wdth_x, trc_wdth_y); 204 | this.ctx.quadraticCurveTo(trc_ctrl_x, trc_ctrl_y, trc_lgth_x, trc_lgth_y); 205 | 206 | // top line 207 | this.ctx.lineTo(tlc_lgth_x, tlc_lgth_y); 208 | this.ctx.fill(); 209 | this.ctx.lineWidth = this.chart.tooltips.borderWidth; 210 | this.ctx.strokeStyle = strokeStyle; 211 | this.ctx.stroke(); 212 | 213 | // draw text; 214 | this.ctx.textBaseline = "middle"; 215 | this.ctx.fillStyle = this.chart.tooltips.text.color; 216 | for (var i=0; i < textlines.length; i++) { 217 | var dim = this.ctx.measureText(textlines[i]); 218 | this.ctx.fillText(textlines[i], x + 5 , y + fontSize*(i+1)); 219 | } 220 | 221 | this.ctx.restore(); 222 | 223 | } 224 | }); -------------------------------------------------------------------------------- /src/glyph/Scribl.seq.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Glyph::Seq** 3 | * 4 | * _Glyph used to letters e.g nucleotides or proteins_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | var Seq = Glyph.extend({ 10 | /** **init** 11 | 12 | * _Constructor, call this with `new seq()`_ 13 | 14 | * @param {String} type - a tag to associate this glyph with 15 | * @param {Int} position - start position of the glyph 16 | * @param {Int} length - length of the glyph 17 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 18 | * @api public 19 | */ 20 | init: function(type, position, length, seq, opts) { 21 | this.seq = seq; 22 | this.insertions = []; 23 | // used to show bar chart like information; range 0.0 - 1.0 24 | this.fraction = 1; 25 | this.fractionLevel = 0.3; // level where seq shows as fraction (in pixels) 26 | this.glyphType = "Seq"; 27 | 28 | this.font = "8px courier"; 29 | this.chars = {}; 30 | this.chars.width = undefined; 31 | this.chars.height = undefined; 32 | this.chars.list = ['A', 'G', 'T', 'C', 'N', '-']; 33 | 34 | this._super(type, position, length, undefined, opts); 35 | }, 36 | 37 | /** **_draw** 38 | 39 | * _private letter specific draw method that gets called by this._super.draw()_ 40 | 41 | * @param [context] - optional canvas.context 42 | * @param [length] - optional length of glyph/feature 43 | * @param [height] - optional height of lane 44 | * @api internal 45 | */ 46 | _draw: function(ctx, length, height) { 47 | 48 | // initialize 49 | var seq = this; 50 | var fraction = 1; 51 | 52 | if (seq.lane.chart.ntsToPixels() <= seq.fractionLevel) 53 | fraction = this.fraction 54 | 55 | // see if optional parameters 56 | var ctx = ctx || seq.ctx; 57 | var length = length || seq.getPixelLength(); 58 | var height = height || seq.getHeight(); 59 | 60 | // get coords 61 | var left = seq.getPixelPositionX(); 62 | var top = seq.getPixelPositionY(); 63 | 64 | // check if nts images have been built 65 | var chars = SCRIBL.chars; 66 | 67 | // check if image chars need to be built for this height 68 | if ( !chars.heights[height] ) { 69 | // build nt images 70 | chars.heights[height] = []; 71 | for (var i=0; i < this.chars.list.length; i++) { 72 | var nt = this.chars.list[i]; 73 | var ntName = nt; 74 | if (nt == '-') { ntName = 'dash'; } 75 | var charName = "nt_" + ntName + '_bg'; 76 | this.createChar(nt, chars.nt_color, chars[charName], height); 77 | } 78 | } 79 | 80 | // Set starting draw position 81 | x = y = 0; 82 | 83 | 84 | if (seq.imgCanvas) { 85 | ctx.drawImage(seq.imgCanvas, left, top - height*fraction, length, height*fraction); 86 | } else { 87 | ctx.save(); 88 | ctx.beginPath(); 89 | ctx.textBaseline = "middle"; 90 | var origFont = ctx.font; 91 | var size = /[\d+px]/.exec(origFont) + 'px'; 92 | ctx.font = size + " courier"; 93 | ctx.fillStyle = 'black'; 94 | ctx.textAlign = 'left'; 95 | var seqPx = this.seq.length * chars.heights[height].width; 96 | 97 | // draw text; 98 | seq.imgCanvas = document.createElement('canvas'); 99 | seq.imgCanvas.width = seqPx; 100 | seq.imgCanvas.height = height; 101 | var tmpCtx = seq.imgCanvas.getContext('2d'); 102 | 103 | var pos = 0; 104 | var k = 0; 105 | for (var i=0; i < this.seq.length; i++) { 106 | if (!chars.heights[height][ this.seq[i] ]) { 107 | this.createChar(this.seq[i], 'black', 'white', height); 108 | } 109 | var charGlyph = this.seq[i]; 110 | if (this.insertions.length > 1) { 111 | var h = 2; 112 | } 113 | if (this.insertions[k] && this.insertions[k]['pos'] != undefined) { 114 | if (this.insertions[k]['pos'] -1 == i ){ 115 | charGlyph += 'rightInsert'; 116 | } else if (this.insertions[k] && this.insertions[k]['pos'] == i){ 117 | charGlyph += 'leftInsert'; 118 | k++; 119 | } 120 | } 121 | 122 | tmpCtx.drawImage(chars.heights[height][ charGlyph ],pos,y); 123 | pos += chars.heights[height].width; 124 | } 125 | 126 | ctx.drawImage(seq.imgCanvas, x, height - height*fraction, length, height*fraction); 127 | //ctx.drawImage(seq.imgCanvas, x, y, length, height); 128 | ctx.font = origFont; 129 | 130 | ctx.restore(); 131 | } 132 | 133 | // this is horrible 134 | // have to draw an outline around the nucleotides 135 | // so that the mousehover will work b\c mousehover 136 | // only works with drawn Paths and not drawn Images :( 137 | ctx.beginPath(); 138 | ctx.moveTo(0,0); 139 | ctx.lineTo(length, y); 140 | ctx.lineTo(length, y + height); 141 | ctx.lineTo(x, y+height); 142 | ctx.lineTo(x, y); 143 | ctx.fillStyle = 'rgba(0,0,0,0)'; 144 | if (seq.lane.chart.ntsToPixels() <= seq.fractionLevel) 145 | ctx.strokeStyle = 'rgba(0,0,0,1)'; 146 | else 147 | ctx.strokeStyle = 'rgba(0,0,0,0)'; 148 | 149 | ctx.stroke(); 150 | ctx.closePath(); 151 | }, 152 | 153 | /** **_createChar** 154 | 155 | * _creates glyphs of a given character_ 156 | 157 | * @param {Char} - the char to create glyph of 158 | * @param {String} - string of char color in rgb or hex 159 | * @param {String} - string of char background color in rgb or hex 160 | * @param {Int} - height of glyph 161 | * @api internal 162 | */ 163 | createChar: function(theChar, color, backgroundColor, height) { 164 | var seq = this; 165 | var chart = seq.lane.track.chart; 166 | var canvas = document.createElement('canvas'); 167 | var ctx = canvas.getContext('2d'); 168 | var buffer = 2; 169 | var fontsize = height - buffer; 170 | ctx.font = fontsize + 'px courier'; 171 | var width = ctx.measureText(theChar).width + buffer; 172 | canvas.height = height; 173 | canvas.width = width; 174 | SCRIBL.chars.heights[height].width = width; 175 | 176 | // draw standard nt 177 | var canvas = document.createElement('canvas'); 178 | var ctx = canvas.getContext('2d'); 179 | ctx.font = fontsize + 'px courier'; 180 | canvas.height = height; 181 | canvas.width = width; 182 | var fillStyle = ctx.fillStyle; 183 | ctx.fillStyle = backgroundColor; 184 | ctx.fillRect(0,0, width, height); 185 | ctx.fillStyle = color; 186 | ctx.textAlign = 'center'; 187 | ctx.textBaseline = "middle"; 188 | ctx.fillText(theChar, width/2, height/2); 189 | // store canvas with glyph in global variable 190 | SCRIBL.chars.heights[height][theChar] = canvas; 191 | ctx.fillStyle = fillStyle; 192 | 193 | // draw nt with insert to the right 194 | var canvas = document.createElement('canvas'); 195 | var ctx = canvas.getContext('2d'); 196 | ctx.font = fontsize + 'px courier'; 197 | canvas.height = height; 198 | canvas.width = width; 199 | var fillStyle = ctx.fillStyle; 200 | ctx.fillStyle = backgroundColor; 201 | ctx.fillRect(0,0, width, height); 202 | ctx.fillStyle = 'yellow'; 203 | ctx.beginPath(); 204 | ctx.moveTo(0,height); 205 | ctx.arcTo(width,height, width,0, height/2); 206 | ctx.lineTo(width,height); 207 | ctx.lineTo(0, height) 208 | ctx.closePath(); 209 | ctx.fill(); 210 | ctx.fillStyle = color; 211 | ctx.textAlign = 'center'; 212 | ctx.textBaseline = "middle"; 213 | ctx.fillText(theChar, width/2, height/2); 214 | // store canvas with glyph in global variable 215 | SCRIBL.chars.heights[height][theChar + 'rightInsert'] = canvas; 216 | ctx.fillStyle = fillStyle; 217 | 218 | // draw nt with insertion to the left 219 | var canvas = document.createElement('canvas'); 220 | var ctx = canvas.getContext('2d'); 221 | ctx.font = fontsize + 'px courier'; 222 | canvas.height = height; 223 | canvas.width = width; 224 | var fillStyle = ctx.fillStyle; 225 | ctx.fillStyle = backgroundColor; 226 | ctx.fillRect(0,0, width, height); 227 | ctx.fillStyle = 'yellow'; 228 | ctx.beginPath(); 229 | ctx.moveTo(width,height); 230 | ctx.arcTo(0,height, 0,0, height/2); 231 | ctx.lineTo(0,height); 232 | ctx.lineTo(width, height) 233 | ctx.closePath(); 234 | ctx.fill(); 235 | ctx.fillStyle = color; 236 | ctx.textAlign = 'center'; 237 | ctx.textBaseline = "middle"; 238 | ctx.fillText(theChar, width/2, height/2); 239 | // store canvas with glyph in global variable 240 | SCRIBL.chars.heights[height][theChar + 'leftInsert'] = canvas; 241 | ctx.fillStyle = fillStyle; 242 | 243 | } 244 | }); -------------------------------------------------------------------------------- /src/Scribl.track.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Scribl::Track 3 | * 4 | * _Tracks are used to segregrate different sequence data_ 5 | * 6 | * Chase Miller 2011 7 | */ 8 | 9 | 10 | var Track = Class.extend({ 11 | /** **init** 12 | 13 | * _Constructor_ 14 | * 15 | * This is called with `new Track()`, but to create new Tracks associated with a chart use `Scribl.addTrack()` 16 | * 17 | * @param {Object} ctx - the canvas.context object 18 | * @api internal 19 | */ 20 | init: function(chart) { 21 | // defaults 22 | var track = this; 23 | this.chart = chart 24 | this.lanes = []; 25 | this.ctx = chart.ctx; 26 | this.uid = _uniqueId('track'); 27 | this.drawStyle = undefined; 28 | this.hide = false; 29 | this.hooks = {}; 30 | 31 | // add draw hooks 32 | for (var i=0; i (prev_feature.position + prev_feature.length) ) { 108 | new_lane = false; 109 | curr_lane = this.lanes[j]; 110 | break; 111 | } 112 | } 113 | 114 | // add new lane if needed 115 | if (new_lane) 116 | curr_lane = this.addLane(); 117 | 118 | // add feature 119 | curr_lane.addFeature( feature ); 120 | return feature; 121 | }, 122 | 123 | /** **hide** 124 | 125 | * _hides the track so it doesn't get drawn_ 126 | 127 | * @api public 128 | */ 129 | hide: function() { 130 | this.hide = true; 131 | }, 132 | 133 | /** **unhide** 134 | 135 | * _unhides the track so it is drawn_ 136 | 137 | * @api public 138 | */ 139 | unhide: function() { 140 | this.hide = false; 141 | }, 142 | 143 | /** **getDrawStyle** 144 | 145 | * _returns the draw style associated with this track_ 146 | 147 | * @return {String} drawStyle - the style this track will be drawn e.g. expand, collapse, line 148 | * @api public 149 | */ 150 | getDrawStyle: function() { 151 | if (this.drawStyle) 152 | return this.drawStyle 153 | else 154 | return this.chart.drawStyle; 155 | }, 156 | 157 | /** **getHeight** 158 | 159 | * _returns the height of this track in pixels_ 160 | 161 | * @return {Int} height 162 | * @api public 163 | */ 164 | getHeight: function() { 165 | var wholeHeight = 0; 166 | 167 | var numLanes = this.lanes.length; 168 | var laneBuffer = this.chart.laneBuffer; 169 | var drawStyle = this.getDrawStyle(); 170 | if (drawStyle == 'line' || drawStyle == 'collapse') 171 | numLanes = 1; 172 | 173 | for (var i=0; i < numLanes; i++) { 174 | wholeHeight += laneBuffer; 175 | wholeHeight += this.lanes[i].getHeight(); 176 | } 177 | // subtract 1 laneBuffer b\c laneBuffers are between lanes 178 | wholeHeight -= laneBuffer; 179 | 180 | return wholeHeight; 181 | }, 182 | 183 | /** **getPixelPositionY** 184 | 185 | * _gets the number of pixels from the top of the chart to the top of this track_ 186 | 187 | * @return {Int} pixelPositionY 188 | * @api public 189 | */ 190 | getPixelPositionY: function() { 191 | var track = this; 192 | var y; 193 | 194 | if (!track.chart.scale.off) 195 | y = track.chart.getScaleHeight() + track.chart.laneBuffer; 196 | else 197 | y = 0; 198 | 199 | for( var i=0; i < track.chart.tracks.length; i++ ) { 200 | if (track.uid == track.chart.tracks[i].uid) break; 201 | y += track.chart.trackBuffer; 202 | y += track.chart.tracks[i].getHeight(); 203 | } 204 | 205 | return y; 206 | }, 207 | 208 | /** **calcCoverageData** 209 | 210 | * _calculates the coverage (the number of features) at each pixel_ 211 | * 212 | * @api internal 213 | */ 214 | calcCoverageData: function() { 215 | var lanes = this.lanes 216 | var min = this.chart.scale.min; 217 | var max = this.chart.scale.max; 218 | 219 | // determine feature locations 220 | for (var i=0; i= min && pos <= max) || (end >= min && end <= max) ) { 226 | var from = Math.round( feature.getPixelPositionX() ); 227 | var to = Math.round( from + feature.getPixelLength() ); 228 | for (var j=from; j <= to; j++) { 229 | this.coverageData[j] = this.coverageData[j] + 1 || 1; 230 | this.maxDepth = Math.max(this.coverageData[j], this.maxDepth); 231 | } 232 | } 233 | } 234 | } 235 | }, 236 | 237 | /** **erase** 238 | 239 | * _erases this track_ 240 | * 241 | * @api internal 242 | */ 243 | erase: function() { 244 | var track = this; 245 | track.chart.ctx.clearRect(0, track.getPixelPositionY(), track.chart.width, track.getHeight()); 246 | }, 247 | 248 | /** **draw** 249 | 250 | * _draws Track_ 251 | 252 | * @api internal 253 | */ 254 | draw: function() { 255 | var track = this; 256 | 257 | // execute hooks 258 | var dontDraw = false; 259 | for (var i in track.hooks) { 260 | dontDraw = track.hooks[i](track) || dontDraw; 261 | } 262 | 263 | // check if track is waiting and if so do nothing 264 | if ( track.status == 'waiting' ) { 265 | track.drawOnResponse = true; 266 | return; 267 | } 268 | 269 | // check if track shouldn't be drawn 270 | if(track.hide) 271 | return; 272 | 273 | var style = track.getDrawStyle(); 274 | var laneSize = track.chart.laneSizes; 275 | var lanes = track.lanes; 276 | var laneBuffer = track.chart.laneBuffer; 277 | var trackBuffer = track.chart.trackBuffer; 278 | var y = laneSize + trackBuffer; 279 | var ctx = track.chart.ctx; 280 | 281 | if (!dontDraw) { 282 | 283 | // draw lanes 284 | 285 | // draw expanded/default style 286 | if ( style == undefined || style == 'expand' ) { 287 | for (var i=0; i= features[m].position) { 311 | // features[j].length = Math.max(features[j].getEnd(), features[m].getEnd()) - features[j].position; 312 | // features[j].name = ""; 313 | // } else break; 314 | // } 315 | // draw 316 | features[j].draw(); 317 | // put length and name back to correct values 318 | features[j].length = originalLength; 319 | features[j].name = originalName; 320 | // update j to skip features that were merged 321 | // j = m-1; 322 | } 323 | // translate down to next lane to draw 324 | if (lanes.length > 0) 325 | ctx.translate(0, lanes[0].getHeight() + laneBuffer); 326 | 327 | // draw as a line chart of the coverage 328 | } else if ( style == 'line' ) { 329 | track.coverageData = []; 330 | if (track.coverageData.length == 0) track.calcCoverageData(); 331 | 332 | var normalizationFactor = this.maxDepth; 333 | 334 | ctx.beginPath(); 335 | // ctx.moveTo(this.chart.offset, laneSize); 336 | for (var k=this.chart.offset; k <= this.chart.width + this.chart.offset; k++) { 337 | var normalizedPt = track.coverageData[k] / normalizationFactor * laneSize || 0; 338 | normalizedPt = laneSize - normalizedPt; 339 | ctx.lineTo(k, normalizedPt); 340 | } 341 | ctx.lineTo(this.chart.width + this.chart.offset, laneSize) 342 | // ctx.lineTo(this.chart.offset, laneSize); 343 | ctx.stroke(); 344 | ctx.translate(0, lanes[0].getHeight() + laneBuffer); 345 | } 346 | } 347 | 348 | // add track buffer - extra laneBuffer 349 | ctx.translate(0,trackBuffer-laneBuffer); 350 | 351 | }, 352 | 353 | /** **addDrawHook** 354 | 355 | * _add function that executes before the track is drawn_ 356 | 357 | * @param {Function} function - takes track as param, returns true to stop the normal draw, false to allow 358 | * @return {Int} id - returns the uniqe id for the hook which is used to remove it 359 | * @api public 360 | */ 361 | 362 | addDrawHook: function(fn, hookId) { 363 | var uid = hookId || _uniqueId('drawHook'); 364 | this.hooks[uid] = fn; 365 | return uid; 366 | }, 367 | 368 | /** **removeDrawHook** 369 | 370 | * _removes function that executes before the track is drawn_ 371 | 372 | * @param {Int} id - the id of drawHook function that will be removed 373 | * @api public 374 | */ 375 | 376 | removeDrawHook: function(uid) { 377 | delete this.hooks[uid]; 378 | } 379 | 380 | }); 381 | -------------------------------------------------------------------------------- /lib/bam.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: javascript; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | 3 | // 4 | // Dalliance Genome Explorer 5 | // (c) Thomas Down 2006-2011 6 | // 7 | // bam.js: indexed binary alignments 8 | // 9 | 10 | var BAM_MAGIC = 21840194; 11 | var BAI_MAGIC = 21578050; 12 | 13 | function BamFile() { 14 | } 15 | 16 | function Vob(b, o) { 17 | this.block = b; 18 | this.offset = o; 19 | } 20 | 21 | Vob.prototype.toString = function() { 22 | return '' + this.block + ':' + this.offset; 23 | } 24 | 25 | function Chunk(minv, maxv) { 26 | this.minv = minv; this.maxv = maxv; 27 | } 28 | 29 | function makeBam(data, bai, callback) { 30 | var bam = new BamFile(); 31 | bam.data = data; 32 | bam.bai = bai; 33 | 34 | bam.data.fetch(function(r) { 35 | if (!r) { 36 | return alert("Couldn't access BAM"); 37 | } 38 | 39 | var unc = unbgzf(r); 40 | var uncba = new Uint8Array(unc); 41 | 42 | var magic = readInt(uncba, 0); 43 | var headLen = readInt(uncba, 4); 44 | var header = ''; 45 | for (var i = 0; i < headLen; ++i) { 46 | header += String.fromCharCode(uncba[i + 8]); 47 | } 48 | 49 | var nRef = readInt(uncba, headLen + 8); 50 | var p = headLen + 12; 51 | 52 | bam.chrToIndex = {}; 53 | bam.indexToChr = []; 54 | for (var i = 0; i < nRef; ++i) { 55 | var lName = readInt(uncba, p); 56 | var name = ''; 57 | for (var j = 0; j < lName-1; ++j) { 58 | name += String.fromCharCode(uncba[p + 4 + j]); 59 | } 60 | var lRef = readInt(uncba, p + lName + 4); 61 | //dlog(name + ': ' + lRef); 62 | bam.chrToIndex[name] = i; 63 | if (name.indexOf('chr') == 0) { 64 | bam.chrToIndex[name.substring(3)] = i; 65 | } else { 66 | bam.chrToIndex['chr' + name] = i; 67 | } 68 | bam.indexToChr.push(name); 69 | 70 | p = p + 8 + lName; 71 | } 72 | 73 | if (bam.indices) { 74 | return callback(bam); 75 | } 76 | }); 77 | 78 | bam.bai.fetch(function(header) { // Do we really need to fetch the whole thing? :-( 79 | if (!header) { 80 | return alert("Couldn't access BAI"); 81 | } 82 | 83 | var uncba = new Uint8Array(header); 84 | var baiMagic = readInt(uncba, 0); 85 | if (baiMagic != BAI_MAGIC) { 86 | return alert('Not a BAI file'); 87 | } 88 | 89 | var nref = readInt(uncba, 4); 90 | 91 | bam.indices = []; 92 | 93 | var p = 8; 94 | for (var ref = 0; ref < nref; ++ref) { 95 | var blockStart = p; 96 | var nbin = readInt(uncba, p); p += 4; 97 | for (var b = 0; b < nbin; ++b) { 98 | var bin = readInt(uncba, p); 99 | var nchnk = readInt(uncba, p+4); 100 | p += 8 + (nchnk * 16); 101 | } 102 | var nintv = readInt(uncba, p); p += 4; 103 | p += (nintv * 8); 104 | if (nbin > 0) { 105 | bam.indices[ref] = new Uint8Array(header, blockStart, p - blockStart); 106 | } 107 | } 108 | if (bam.chrToIndex) { 109 | return callback(bam); 110 | } 111 | }); 112 | } 113 | 114 | 115 | 116 | BamFile.prototype.blocksForRange = function(refId, min, max) { 117 | var index = this.indices[refId]; 118 | if (!index) { 119 | return []; 120 | } 121 | 122 | var intBinsL = reg2bins(min, max); 123 | var intBins = []; 124 | for (var i = 0; i < intBinsL.length; ++i) { 125 | intBins[intBinsL[i]] = true; 126 | } 127 | var leafChunks = [], otherChunks = []; 128 | 129 | var nbin = readInt(index, 0); 130 | var p = 4; 131 | for (var b = 0; b < nbin; ++b) { 132 | var bin = readInt(index, p); 133 | var nchnk = readInt(index, p+4); 134 | // dlog('bin=' + bin + '; nchnk=' + nchnk); 135 | p += 8; 136 | if (intBins[bin]) { 137 | for (var c = 0; c < nchnk; ++c) { 138 | var cs = readVob(index, p); 139 | var ce = readVob(index, p + 8); 140 | (bin < 4681 ? otherChunks : leafChunks).push(new Chunk(cs, ce)); 141 | p += 16; 142 | } 143 | } else { 144 | p += (nchnk * 16); 145 | } 146 | } 147 | // dlog('leafChunks = ' + miniJSONify(leafChunks)); 148 | // dlog('otherChunks = ' + miniJSONify(otherChunks)); 149 | 150 | var nintv = readInt(index, p); 151 | var lowest = null; 152 | var minLin = Math.min(min>>14, nintv - 1), maxLin = Math.min(max>>14, nintv - 1); 153 | for (var i = minLin; i <= maxLin; ++i) { 154 | var lb = readVob(index, p + 4 + (i * 8)); 155 | if (!lb) { 156 | continue; 157 | } 158 | if (!lowest || lb.block < lowest.block || lb.offset < lowest.offset) { 159 | lowest = lb; 160 | } 161 | } 162 | // dlog('Lowest LB = ' + lowest); 163 | 164 | var prunedOtherChunks = []; 165 | if (lowest != null) { 166 | for (var i = 0; i < otherChunks.length; ++i) { 167 | var chnk = otherChunks[i]; 168 | if (chnk.maxv.block >= lowest.block && chnk.maxv.offset >= lowest.offset) { 169 | prunedOtherChunks.push(chnk); 170 | } 171 | } 172 | } 173 | // dlog('prunedOtherChunks = ' + miniJSONify(prunedOtherChunks)); 174 | otherChunks = prunedOtherChunks; 175 | 176 | var intChunks = []; 177 | for (var i = 0; i < otherChunks.length; ++i) { 178 | intChunks.push(otherChunks[i]); 179 | } 180 | for (var i = 0; i < leafChunks.length; ++i) { 181 | intChunks.push(leafChunks[i]); 182 | } 183 | 184 | intChunks.sort(function(c0, c1) { 185 | var dif = c0.minv.block - c1.minv.block; 186 | if (dif != 0) { 187 | return dif; 188 | } else { 189 | return c0.minv.offset - c1.minv.offset; 190 | } 191 | }); 192 | var mergedChunks = []; 193 | if (intChunks.length > 0) { 194 | var cur = intChunks[0]; 195 | for (var i = 1; i < intChunks.length; ++i) { 196 | var nc = intChunks[i]; 197 | if (nc.minv.block == cur.maxv.block /* && nc.minv.offset == cur.maxv.offset */) { // no point splitting mid-block 198 | cur = new Chunk(cur.minv, nc.maxv); 199 | } else { 200 | mergedChunks.push(cur); 201 | cur = nc; 202 | } 203 | } 204 | mergedChunks.push(cur); 205 | } 206 | // dlog('mergedChunks = ' + miniJSONify(mergedChunks)); 207 | 208 | return mergedChunks; 209 | } 210 | 211 | BamFile.prototype.fetch = function(chr, min, max, callback) { 212 | var thisB = this; 213 | 214 | var chrId = this.chrToIndex[chr]; 215 | var chunks; 216 | if (chrId === undefined) { 217 | chunks = []; 218 | } else { 219 | chunks = this.blocksForRange(chrId, min, max); 220 | if (!chunks) { 221 | callback(null, 'Error in index fetch'); 222 | } 223 | } 224 | 225 | var records = []; 226 | var index = 0; 227 | var data; 228 | 229 | function tramp() { 230 | if (index >= chunks.length) { 231 | return callback(records); 232 | } else if (!data) { 233 | // dlog('fetching ' + index); 234 | var c = chunks[index]; 235 | var fetchMin = c.minv.block; 236 | var fetchMax = c.maxv.block + (1<<16); // *sigh* 237 | thisB.data.slice(fetchMin, fetchMax - fetchMin).fetch(function(r) { 238 | data = unbgzf(r, c.maxv.block - c.minv.block + 1); 239 | return tramp(); 240 | }); 241 | } else { 242 | var ba = new Uint8Array(data); 243 | thisB.readBamRecords(ba, chunks[index].minv.offset, records, min, max, chrId); 244 | data = null; 245 | ++index; 246 | return tramp(); 247 | } 248 | } 249 | tramp(); 250 | } 251 | 252 | var SEQRET_DECODER = ['=', 'A', 'C', 'x', 'G', 'x', 'x', 'x', 'T', 'x', 'x', 'x', 'x', 'x', 'x', 'N']; 253 | var CIGAR_DECODER = ['M', 'I', 'D', 'N', 'S', 'H', 'P', '=', 'X', '?', '?', '?', '?', '?', '?', '?']; 254 | 255 | function BamRecord() { 256 | } 257 | 258 | BamFile.prototype.readBamRecords = function(ba, offset, sink, min, max, chrId) { 259 | while (true) { 260 | var blockSize = readInt(ba, offset); 261 | var blockEnd = offset + blockSize + 4; 262 | if (blockEnd >= ba.length) { 263 | return sink; 264 | } 265 | 266 | var record = new BamRecord(); 267 | 268 | var refID = readInt(ba, offset + 4); 269 | var pos = readInt(ba, offset + 8); 270 | 271 | var bmn = readInt(ba, offset + 12); 272 | var bin = (bmn & 0xffff0000) >> 16; 273 | var mq = (bmn & 0xff00) >> 8; 274 | var nl = bmn & 0xff; 275 | 276 | var flag_nc = readInt(ba, offset + 16); 277 | var flag = (flag_nc & 0xffff0000) >> 16; 278 | var nc = flag_nc & 0xffff; 279 | 280 | var lseq = readInt(ba, offset + 20); 281 | 282 | var nextRef = readInt(ba, offset + 24); 283 | var nextPos = readInt(ba, offset + 28); 284 | 285 | var tlen = readInt(ba, offset + 32); 286 | 287 | var readName = ''; 288 | for (var j = 0; j < nl-1; ++j) { 289 | readName += String.fromCharCode(ba[offset + 36 + j]); 290 | } 291 | 292 | var p = offset + 36 + nl; 293 | 294 | var lengthOnRef = 0; 295 | var cigar = ''; 296 | for (var c = 0; c < nc; ++c) { 297 | var cigop = readInt(ba, p); 298 | var op = (cigop>>4); 299 | var opLtr = CIGAR_DECODER[cigop & 0xf]; 300 | if (opLtr == 'M' || opLtr == 'I' || opLtr == 'X') 301 | lengthOnRef += op; 302 | cigar = cigar + op + opLtr; 303 | p += 4; 304 | } 305 | record.cigar = cigar; 306 | record.lengthOnRef = lengthOnRef; 307 | 308 | var seq = ''; 309 | var seqBytes = (lseq + 1) >> 1; 310 | for (var j = 0; j < seqBytes; ++j) { 311 | var sb = ba[p + j]; 312 | seq += SEQRET_DECODER[(sb & 0xf0) >> 4]; 313 | seq += SEQRET_DECODER[(sb & 0x0f)]; 314 | } 315 | p += seqBytes; 316 | record.seq = seq; 317 | 318 | var qseq = ''; 319 | for (var j = 0; j < lseq; ++j) { 320 | qseq += String.fromCharCode(ba[p + j]); 321 | } 322 | p += lseq; 323 | record.quals = qseq; 324 | 325 | record.pos = pos; 326 | record.mq = mq; 327 | record.readName = readName; 328 | record.segment = this.indexToChr[refID]; 329 | 330 | while (p < blockEnd) { 331 | var tag = String.fromCharCode(ba[p]) + String.fromCharCode(ba[p + 1]); 332 | var type = String.fromCharCode(ba[p + 2]); 333 | var value; 334 | 335 | if (type == 'A') { 336 | value = String.fromCharCode(ba[p + 3]); 337 | p += 4; 338 | } else if (type == 'i' || type == 'I') { 339 | value = readInt(ba, p + 3); 340 | p += 7; 341 | } else if (type == 'c' || type == 'C') { 342 | value = ba[p + 3]; 343 | p += 4; 344 | } else if (type == 's' || type == 'S') { 345 | value = readShort(ba, p + 3); 346 | p += 5; 347 | } else if (type == 'f') { 348 | throw 'FIXME need floats'; 349 | } else if (type == 'Z') { 350 | p += 3; 351 | value = ''; 352 | for (;;) { 353 | var cc = ba[p++]; 354 | if (cc == 0) { 355 | break; 356 | } else { 357 | value += String.fromCharCode(cc); 358 | } 359 | } 360 | } else { 361 | throw 'Unknown type '+ type; 362 | } 363 | record[tag] = value; 364 | } 365 | 366 | if (!min || record.pos <= max && record.pos + lseq >= min) { 367 | if (chrId === undefined || refID == chrId) { 368 | sink.push(record); 369 | } 370 | } 371 | offset = blockEnd; 372 | } 373 | 374 | // Exits via top of loop. 375 | } 376 | 377 | function readInt(ba, offset) { 378 | return (ba[offset + 3] << 24) | (ba[offset + 2] << 16) | (ba[offset + 1] << 8) | (ba[offset]); 379 | } 380 | 381 | function readShort(ba, offset) { 382 | return (ba[offset + 1] << 8) | (ba[offset]); 383 | } 384 | 385 | function readVob(ba, offset) { 386 | var block = ((ba[offset+6] & 0xff) * 0x100000000) + ((ba[offset+5] & 0xff) * 0x1000000) + ((ba[offset+4] & 0xff) * 0x10000) + ((ba[offset+3] & 0xff) * 0x100) + ((ba[offset+2] & 0xff)); 387 | var bint = (ba[offset+1] << 8) | (ba[offset]); 388 | if (block == 0 && bint == 0) { 389 | return null; // Should only happen in the linear index? 390 | } else { 391 | return new Vob(block, bint); 392 | } 393 | } 394 | 395 | function unbgzf(data, lim) { 396 | lim = Math.min(lim || 1, data.byteLength - 100); 397 | var oBlockList = []; 398 | var ptr = [0]; 399 | var totalSize = 0; 400 | 401 | while (ptr[0] < lim) { 402 | var ba = new Uint8Array(data, ptr[0], 100); // FIXME is this enough for all credible BGZF block headers? 403 | var xlen = (ba[11] << 8) | (ba[10]); 404 | // dlog('xlen[' + (ptr[0]) +']=' + xlen); 405 | var unc = jszlib_inflate_buffer(data, 12 + xlen + ptr[0], Math.min(65536, data.byteLength - 12 - xlen - ptr[0]), ptr); 406 | ptr[0] += 8; 407 | totalSize += unc.byteLength; 408 | oBlockList.push(unc); 409 | } 410 | 411 | if (oBlockList.length == 1) { 412 | return oBlockList[0]; 413 | } else { 414 | var out = new Uint8Array(totalSize); 415 | var cursor = 0; 416 | for (var i = 0; i < oBlockList.length; ++i) { 417 | var b = new Uint8Array(oBlockList[i]); 418 | arrayCopy(b, 0, out, cursor, b.length); 419 | cursor += b.length; 420 | } 421 | return out.buffer; 422 | } 423 | } 424 | 425 | // 426 | // Binning (transliterated from SAM1.3 spec) 427 | // 428 | 429 | /* calculate bin given an alignment covering [beg,end) (zero-based, half-close-half-open) */ 430 | function reg2bin(beg, end) 431 | { 432 | --end; 433 | if (beg>>14 == end>>14) return ((1<<15)-1)/7 + (beg>>14); 434 | if (beg>>17 == end>>17) return ((1<<12)-1)/7 + (beg>>17); 435 | if (beg>>20 == end>>20) return ((1<<9)-1)/7 + (beg>>20); 436 | if (beg>>23 == end>>23) return ((1<<6)-1)/7 + (beg>>23); 437 | if (beg>>26 == end>>26) return ((1<<3)-1)/7 + (beg>>26); 438 | return 0; 439 | } 440 | 441 | /* calculate the list of bins that may overlap with region [beg,end) (zero-based) */ 442 | var MAX_BIN = (((1<<18)-1)/7); 443 | function reg2bins(beg, end) 444 | { 445 | var i = 0, k, list = []; 446 | --end; 447 | list.push(0); 448 | for (k = 1 + (beg>>26); k <= 1 + (end>>26); ++k) list.push(k); 449 | for (k = 9 + (beg>>23); k <= 9 + (end>>23); ++k) list.push(k); 450 | for (k = 73 + (beg>>20); k <= 73 + (end>>20); ++k) list.push(k); 451 | for (k = 585 + (beg>>17); k <= 585 + (end>>17); ++k) list.push(k); 452 | for (k = 4681 + (beg>>14); k <= 4681 + (end>>14); ++k) list.push(k); 453 | return list; 454 | } -------------------------------------------------------------------------------- /src/Scribl.glyph.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl::Glyph** 3 | * 4 | * _Generic glyph class that should not be used directly. 5 | * All feature classes (e.g. Rect, arrow, etc..) inherit 6 | * from this class_ 7 | * 8 | * Chase Miller 2011 9 | * 10 | */ 11 | 12 | var Glyph = Class.extend({ 13 | /** **init** 14 | 15 | * _Constructor, call this with `new Glyph()`_ 16 | * This method must be called in all feature subclasses like so `this._super(type, pos, length, strand, opts)` 17 | 18 | * @param {String} type - a tag to associate this glyph with 19 | * @param {Int} position - start position of the glyph 20 | * @param {Int} length - length of the glyph 21 | * @param {String} strand - '+' or '-' strand 22 | * @param {Hash} [opts] - optional hash of attributes that can be applied to glyph 23 | * @api internal 24 | */ 25 | init: function(type, pos, length, strand, opts) { 26 | var glyph = this; 27 | 28 | // set unique id 29 | this.uid = _uniqueId('feature'); 30 | 31 | // set variables 32 | glyph.position = pos; 33 | glyph.length = length; 34 | glyph.strand = strand; 35 | // this is used for all attributes at the chart level (e.g. chart.gene.color = "blue" ) 36 | this.type = type; 37 | glyph.opts = {}; 38 | 39 | glyph.name = ""; 40 | glyph.borderColor = "none"; 41 | glyph.borderWidth = undefined; 42 | glyph.ntLevel = 4; // in pixels - sets the level at which glyphs are rendered as actual nucleotides instead of icons 43 | glyph.tooltips = []; 44 | glyph.hooks = {}; 45 | 46 | // add seq hook 47 | glyph.addDrawHook(function(theGlyph) { 48 | if (theGlyph.ntLevel != undefined && theGlyph.seq && theGlyph.lane.chart.ntsToPixels() < theGlyph.ntLevel){ 49 | var s = new Seq(theGlyph.type, theGlyph.position, theGlyph.length, theGlyph.seq, theGlyph.opts); 50 | s.lane = theGlyph.lane; 51 | s.ctx = theGlyph.ctx; 52 | s._draw(); 53 | // return true to stop normal drawing of glyph 54 | return true; 55 | } 56 | // return false to allow normal draing of glyph 57 | return false; 58 | }, "ntHook"); 59 | 60 | // initialize font variables 61 | glyph.text = {}; 62 | // unset defaults that can be used to override chart defaults for specific glyphs 63 | glyph.text.font = undefined; // default: 'arial' 64 | glyph.text.size = undefined; // default: '15' in pixels 65 | glyph.text.color = undefined; // default: 'black' 66 | glyph.text.align = undefined; // default: 'middle' 67 | 68 | glyph.onClick = undefined; 69 | glyph.onMouseover = undefined; 70 | 71 | // set option attributes if any 72 | for (var attribute in opts) { 73 | glyph[attribute] = opts[attribute]; 74 | glyph.opts[attribute] = opts[attribute]; 75 | } 76 | 77 | }, 78 | 79 | /** **setColorGradient** 80 | 81 | * _creates a gradient given a list of colors_ 82 | 83 | * @param {List} colors - takes as many colors as you like 84 | * @api public 85 | */ 86 | setColorGradient: function() { 87 | if(arguments.length == 1){ 88 | this.color = arguments[0]; 89 | return; 90 | } 91 | var lineargradient = this.lane.ctx.createLinearGradient(this.length/2, 0, this.length/2, this.getHeight()); 92 | var color; 93 | for(var i=0; color=arguments[i], i < arguments.length; i++){ 94 | lineargradient.addColorStop(i / (arguments.length-1), color); 95 | } 96 | this.color = lineargradient; 97 | }, 98 | 99 | /** **getPixelLength** 100 | 101 | * _gets the length of the glyph/feature in pixels_ 102 | 103 | * @return {Int} length - in pixels 104 | * @api public 105 | */ 106 | getPixelLength: function() { 107 | var glyph = this; 108 | return ( glyph.lane.chart.pixelsToNts(glyph.length) || 1 ); 109 | }, 110 | 111 | 112 | /** **getPixelPositionx** 113 | 114 | * _gets the number of pixels from the left of the chart to the left of this glyph/feature_ 115 | 116 | * @return {Float} positionX - in pixels 117 | * @api public 118 | */ 119 | getPixelPositionX: function() { 120 | var glyph = this; 121 | var offset = parseInt(glyph.lane.track.chart.offset) || 0; 122 | if (glyph.parent) 123 | var position = glyph.position + glyph.parent.position - glyph.lane.track.chart.scale.min; 124 | else 125 | var position = glyph.position - glyph.lane.track.chart.scale.min; 126 | return ( glyph.lane.track.chart.pixelsToNts( position ) + offset); 127 | }, 128 | 129 | /** **getPixelPositionY** 130 | 131 | * _gets the number of pixels from the top of the chart to the top of this glyph/feature_ 132 | 133 | * @return {Float} positionY - in pixels 134 | * @api public 135 | */ 136 | getPixelPositionY : function() { 137 | var glyph = this; 138 | return (glyph.lane.getPixelPositionY()); 139 | }, 140 | 141 | /** **getEnd** 142 | 143 | * _gets the nucleotide/amino acid end point of this glyph/feature_ 144 | 145 | * @return {Int} end - in nucleotides/amino acids 146 | * @api public 147 | */ 148 | getEnd: function() { 149 | return (this.position + this.length); 150 | }, 151 | 152 | /** **clone** 153 | 154 | * _shallow copy_ 155 | 156 | * @return {Object} copy - shallow copy of this glyph/feature 157 | * @api public 158 | */ 159 | clone: function(glyphType) { 160 | var glyph = this; 161 | var newFeature; 162 | 163 | glyphType = glyphType || glyph.glyphType; 164 | 165 | if (glyphType == "Rect" || glyphType == "Line") 166 | glyph.strand = undefined 167 | 168 | if(glyph.strand){ 169 | var str = 'new ' + glyphType + '("' + glyph.type + '",' + glyph.position + ',' + glyph.length + ',"' + glyph.strand + '",' + JSON.stringify(glyph.opts) + ')'; 170 | newFeature = eval( str ); 171 | var attrs = Object.keys(glyph); 172 | for ( var i=0; i < attrs.length; i++) { 173 | newFeature[attrs[i]] = glyph[attrs[i]]; 174 | } 175 | } else { 176 | var str = 'new ' + glyphType + '("' + glyph.type + '",' + glyph.position + ',' + glyph.length + ',' + JSON.stringify(glyph.opts) + ')'; 177 | newFeature = eval(str); 178 | var attrs = Object.keys(glyph); 179 | for ( var i=0; i < attrs.length; i++) { 180 | newFeature[attrs[i]] = glyph[attrs[i]]; 181 | } 182 | } 183 | 184 | newFeature.tooltips = glyph.tooltips; 185 | newFeature.hooks = glyph.hooks; 186 | return( newFeature ); 187 | 188 | }, 189 | 190 | /** **getAttr** 191 | 192 | * _determine and retrieve the appropriate value for each attribute, checks parent, default, type, and glyph levels in the appropriate order_ 193 | 194 | * @param {*} attribute 195 | * @return {*} attribute 196 | * @api public 197 | */ 198 | getAttr : function(attr) { 199 | var glyph = this; 200 | var attrs = attr.split('-'); 201 | 202 | // glyph level 203 | var glyphLevel = glyph; 204 | for( var k=0; k < attrs.length; k++) { glyphLevel = glyphLevel[attrs[k]]; } 205 | if (glyphLevel) return glyphLevel 206 | 207 | // parent level 208 | if (glyph.parent) { 209 | var parentLevel = glyph.parent; 210 | for( var k=0; k < attrs.length; k++) { parentLevel = parentLevel[attrs[k]]; } 211 | if (parentLevel) return parentLevel; 212 | } 213 | 214 | // type level 215 | var typeLevel = this.lane.chart[glyph.type]; 216 | if (typeLevel) { 217 | for( var k=0; k < attrs.length; k++) { typeLevel = typeLevel[attrs[k]]; } 218 | if (typeLevel) return typeLevel; 219 | } 220 | 221 | // chart level 222 | var chartLevel = glyph.lane.chart.glyph; 223 | for( var k=0; k < attrs.length; k++) { chartLevel = chartLevel[attrs[k]]; } 224 | if (chartLevel) return chartLevel; 225 | 226 | // nothing 227 | return undefined; 228 | }, 229 | 230 | /** **drawText** 231 | 232 | * _draws the text for a glyph/feature 233 | _ 234 | * @param {String} text 235 | * @api internal 236 | */ 237 | drawText : function(text) { 238 | // initialize 239 | var glyph = this; 240 | var ctx = glyph.lane.chart.ctx; 241 | var padding = 5; 242 | var length = glyph.getPixelLength(); 243 | var height = glyph.getHeight(); 244 | var fontSize = glyph.getAttr('text-size'); 245 | var fontSizeMin = 8; 246 | var fontStyle = glyph.getAttr('text-style'); 247 | // set ctx 248 | ctx.font = fontSize + "px " + fontStyle; 249 | ctx.textBaseline = "middle"; 250 | ctx.fillStyle = glyph.getAttr('text-color'); 251 | 252 | 253 | // align text properly 254 | var placement = undefined 255 | 256 | // handle relative text alignment based on glyph orientation 257 | var align = glyph.getAttr('text-align'); 258 | if ( align == "start") 259 | if ( glyph.strand == '+' ) 260 | align = 'left'; 261 | else 262 | align = 'right'; 263 | else if ( align == "end" ) 264 | if ( glyph.strand == '+' ) 265 | align = 'right'; 266 | else 267 | align = 'left'; 268 | 269 | // handle absolute text alignment 270 | ctx.textAlign = align; 271 | if (align == 'left') 272 | placement = 0 + padding; 273 | else if ( align == 'center' ) 274 | placement = length/2; 275 | else if ( align == "right" ) 276 | placement = length - padding; 277 | 278 | // test if text size is too big and if so make it smaller 279 | var dim = ctx.measureText(text); 280 | if (text && text != "") { 281 | while ( (length-dim.width) < 4 ) { 282 | fontSize = /^\d+/.exec(ctx.font); 283 | fontSize--; 284 | dim = ctx.measureText(text); 285 | ctx.font = fontSize + "px " + fontStyle; 286 | 287 | // Check if font is getting too small 288 | if (fontSize <= fontSizeMin) { 289 | text = ""; // set name to blank if glyph is too small to display text 290 | break; 291 | } 292 | } 293 | 294 | // handle special case 295 | if (glyph.glyphType == "Complex") { 296 | var offset = 0; 297 | var fontsize = /^\d+/.exec(ctx.font); 298 | if (align == "center") 299 | offset = -(ctx.measureText(text).width/2 + padding/2); 300 | ctx.clearRect(placement + offset, height/2 - fontsize/2, ctx.measureText(text).width + padding, fontsize); 301 | } 302 | ctx.fillText(text, placement, height/2); 303 | } 304 | }, 305 | 306 | /** **calcRoundness** 307 | 308 | * _determines a roundness value based on the height of the glyph feature, so roundness looks consistent as lane size changes_ 309 | 310 | * @return {Int} roundness 311 | * @api internal 312 | */ 313 | calcRoundness : function() { 314 | var roundness = this.getHeight() * this.getAttr('roundness')/100; 315 | // round roundness to the nearest 0.5 316 | roundness = ((roundness*10 % 5) >= 2.5 ? parseInt(roundness*10 / 5) * 5 + 5 : parseInt(roundness*10 / 5) * 5) / 10; 317 | return (roundness); 318 | }, 319 | 320 | /** **isContainedWithinRect** 321 | 322 | * _determines if this glyph/feature is contained within a box with the given coordinates_ 323 | 324 | * @param {Int} selectionTlX - top left X coordinate of bounding box 325 | * @param {Int} selectionTlY - top left Y coordinate of bounding box 326 | * @param {Int} selectionBrX - bottom right X coordinate of bounding box 327 | * @param {Int} selectionBrY - bottom right Y coordinate of bounding box 328 | * @return {Boolean} isContained 329 | * @api public 330 | */ 331 | isContainedWithinRect : function(selectionTlX, selectionTlY, selectionBrX, selectionBrY) { 332 | var glyph = this; 333 | var y = glyph.getPixelPositionY(); 334 | var tlX = glyph.getPixelPositionX(); 335 | var tlY = y 336 | var brX = glyph.getPixelPositionX() + glyph.getPixelLength(); 337 | var brY = y + glyph.getHeight(); 338 | return tlX >= selectionTlX 339 | && brX <= selectionBrX 340 | && tlY >= selectionTlY 341 | && brY <= selectionBrY; 342 | }, 343 | 344 | /** **getHeight** 345 | 346 | * _returns the height of this glyph/feature in pixels_ 347 | 348 | * @return {Int} height 349 | * @api public 350 | */ 351 | getHeight : function() { 352 | var glyph = this; 353 | return ( glyph.lane.getHeight() ); 354 | }, 355 | 356 | /** **getFillStyle** 357 | 358 | * _converts glyph.color into the format taken by canvas.context.fillStyle_ 359 | 360 | * @return {Sting/Object} fillStyle 361 | * @api public 362 | */ 363 | getFillStyle : function() { 364 | var glyph = this; 365 | var color = glyph.getAttr('color'); 366 | 367 | if (color instanceof Array) { 368 | var lineargradient = this.lane.track.chart.ctx.createLinearGradient(this.length/2, 0, this.length/2, this.getHeight()); 369 | var currColor; 370 | for(var i=0; currColor=color[i], i < color.length; i++) 371 | lineargradient.addColorStop(i / (color.length-1), currColor); 372 | return lineargradient 373 | } else if ( color instanceof Function) { 374 | var lineargradient = this.lane.track.chart.ctx.createLinearGradient(this.length/2, 0, this.length/2, this.getHeight()); 375 | return color(lineargradient); 376 | } else 377 | return color; 378 | }, 379 | 380 | /** **getStrokeStyle** 381 | 382 | * _converts glyph.borderColor into the format taken by canvas.context.fillStyle_ 383 | 384 | * @return {Sting/Object} fillStyle 385 | * @api public 386 | */ 387 | getStrokeStyle : function() { 388 | var glyph = this; 389 | var color = glyph.getAttr('borderColor'); 390 | 391 | if (typeof(color) == "object") { 392 | var lineargradient = this.lane.ctx.createLinearGradient(this.length/2, 0, this.length/2, this.getHeight()); 393 | var currColor; 394 | for(var i=0; currColor=color[i], i < color.length; i++) 395 | lineargradient.addColorStop(i / (color.length-1), currColor); 396 | return lineargradient 397 | } else 398 | return color; 399 | }, 400 | 401 | /** **isSubFeature** 402 | 403 | * _checks if glyph/feature has a parent_ 404 | 405 | * @return {Boolean} isSubFeature? 406 | * @api public 407 | */ 408 | isSubFeature: function() { 409 | return (this.parent != undefined); 410 | }, 411 | 412 | /** **erase** 413 | 414 | * _erase this glyph/feature_ 415 | 416 | * @api public 417 | */ 418 | erase: function() { 419 | var glyph = this; 420 | glyph.ctx.save(); 421 | glyph.ctx.setTransform(1,0,0,1,0,0); 422 | glyph.ctx.clearRect(glyph.getPixelPositionX(), glyph.getPixelPositionY(), glyph.getPixelLength(), glyph.getHeight()); 423 | glyph.ctx.restore(); 424 | }, 425 | 426 | /** **addDrawHook** 427 | 428 | * _add function to glyph that executes before the glyph is drawn_ 429 | 430 | * @param {Function} function - takes glyph as param, returns true to stop the normal draw, false to allow 431 | * @return {Int} id - returns the uniqe id for the hook which is used to remove it 432 | * @api public 433 | */ 434 | 435 | addDrawHook: function(fn, hookId) { 436 | var uid = hookId || _uniqueId('drawHook'); 437 | this.hooks[uid] = fn; 438 | return uid; 439 | }, 440 | 441 | /** **removeDrawHook** 442 | 443 | * _removes function to glyph that executes before the glyph is drawn_ 444 | 445 | * @param {Int} id - the id of drawHook function that will be removed 446 | * @api public 447 | */ 448 | 449 | removeDrawHook: function(uid) { 450 | delete this.hooks[uid]; 451 | }, 452 | 453 | /** **addTooltip** 454 | 455 | * _add tooltip to glyph. Can add multiple tooltips_ 456 | 457 | * @param {Int} placement - two options 'above' glyph or 'below' glyph 458 | * @param {Int} verticalOffset - + numbers for up, - for down 459 | * @param {Hash} options - optional attributes, horizontalOffset and ntOffset (nucleotide) 460 | * @return {Object} tooltip 461 | * @api public 462 | */ 463 | 464 | addTooltip: function(text, placement, verticalOffset, opts){ 465 | var glyph = this; 466 | var tt = new Tooltip(text, placement, verticalOffset, opts); 467 | tt.feature = glyph; 468 | glyph.tooltips.push( tt ); 469 | }, 470 | 471 | /** **fireTooltips** 472 | 473 | * _draws the tooltips associated with this feature_ 474 | 475 | * @api public 476 | */ 477 | fireTooltips: function() { 478 | for (var i=0; i < this.tooltips.length; i++) 479 | this.tooltips[i].fire() 480 | }, 481 | 482 | /** **draw** 483 | 484 | * _draws the glyph_ 485 | 486 | * @api internal 487 | */ 488 | draw: function() { 489 | var glyph = this; 490 | 491 | // set ctx 492 | glyph.ctx = glyph.lane.chart.ctx; 493 | glyph.ctx.beginPath(); 494 | 495 | // intialize 496 | var fontSize = /^\d+/.exec(glyph.ctx.font); 497 | var font = /\S+$/.exec(glyph.ctx.font); 498 | var fontSizeMin = 10; 499 | glyph.onClick = glyph.getAttr('onClick'); 500 | glyph.onMouseover = glyph.getAttr('onMouseover'); 501 | glyph.ctx.fillStyle = glyph.getFillStyle(); 502 | var fillStyle = glyph.ctx.fillStyle; 503 | var position = glyph.getPixelPositionX(); 504 | var height = glyph.getHeight(); 505 | 506 | (height < fontSizeMin) ? glyph.ctx.font = fontSizeMin + "px " + font : glyph.ctx.font = height *.9 + "px " + font; 507 | 508 | // setup ctx position and orientation 509 | glyph.ctx.translate(position, 0); 510 | if (glyph.strand == '-' && !glyph.isSubFeature()) 511 | glyph.ctx.transform(-1, 0, 0, 1, glyph.getPixelLength(), 0); 512 | 513 | var dontDraw = false; 514 | for (var i in glyph.hooks) { 515 | dontDraw = glyph.hooks[i](glyph) || dontDraw; 516 | } 517 | if (!dontDraw) { 518 | // draw glyph with subclass specific draw 519 | glyph._draw(); 520 | } 521 | 522 | 523 | // draw border color 524 | if (glyph.borderColor != "none") { 525 | if(glyph.color == 'none' && glyph.parent.glyphType == 'Complex') { 526 | glyph.erase(); 527 | } 528 | var saveStrokeStyle = glyph.ctx.strokeStyle; 529 | var saveLineWidth = glyph.ctx.lineWidth; 530 | glyph.ctx.strokeStyle = glyph.getStrokeStyle(); 531 | glyph.ctx.lineWidth = glyph.getAttr('borderWidth'); 532 | glyph.ctx.stroke(); 533 | glyph.ctx.strokeStyle = saveStrokeStyle; 534 | glyph.ctx.lineWidth = saveLineWidth; 535 | } 536 | 537 | // draw fill color 538 | if (glyph.color !="none") glyph.ctx.fill(); 539 | 540 | // explicity change transformation matrix back -- it's faster than save restore! 541 | if (glyph.strand == '-' && !glyph.isSubFeature()) 542 | glyph.ctx.transform(-1, 0, 0, 1, glyph.getPixelLength(), 0); 543 | 544 | // draw text 545 | glyph.drawText(glyph.getAttr('name')); 546 | 547 | // explicity change transformation matrix back -- it's faster than save restore! 548 | glyph.ctx.translate(-position, 0); 549 | glyph.ctx.fillStyle = fillStyle; 550 | 551 | // setup mouse events if need be 552 | glyph.lane.chart.myMouseEventHandler.addEvents(this); 553 | 554 | }, 555 | 556 | /** **redraw** 557 | 558 | * _erases this specific glyph and redraws it_ 559 | 560 | * @api internal 561 | */ 562 | redraw: function() { 563 | var glyph = this; 564 | glyph.lane.ctx.save(); 565 | glyph.erase; 566 | var y = glyph.getPixelPositionY(); 567 | glyph.lane.ctx.translate(0, y); 568 | glyph.draw(); 569 | glyph.lane.ctx.restore(); 570 | } 571 | 572 | }); 573 | -------------------------------------------------------------------------------- /src/Scribl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * **Scribl Class** 3 | * 4 | * _sets defaults, defines how to add features 5 | * to chart/view and some methods to help 6 | * coordinate drawing_ 7 | * 8 | * Chase Miller 2011 9 | */ 10 | 11 | // globals 12 | // if (SCRIBL == undefined) { 13 | var SCRIBL = {}; 14 | SCRIBL.chars = {}; 15 | SCRIBL.chars.nt_color = 'white'; 16 | SCRIBL.chars.nt_A_bg = 'red'; 17 | SCRIBL.chars.nt_G_bg = 'blue'; 18 | SCRIBL.chars.nt_C_bg = 'green'; 19 | SCRIBL.chars.nt_T_bg = 'black'; 20 | SCRIBL.chars.nt_N_bg = 'purple'; 21 | SCRIBL.chars.nt_dash_bg = 'rgb(120,120,120)'; 22 | SCRIBL.chars.heights = []; 23 | SCRIBL.chars.canvasHolder = document.createElement('canvas'); 24 | //} 25 | 26 | 27 | var Scribl = Class.extend({ 28 | 29 | /** **init** 30 | 31 | * _ Constructor, call this with `new Scribl()`_ 32 | 33 | * @param {Object} canvasHTML object 34 | * @param {Int} width of chart in pixels 35 | * @return {Object} Scribl object 36 | * @api public 37 | */ 38 | init: function(canvas, width) { 39 | this.scrolled = false; 40 | // create canvas contexts 41 | var ctx; 42 | if (canvas) 43 | ctx = canvas.getContext('2d'); 44 | var chart = this; 45 | 46 | // chart defaults 47 | this.width = width; 48 | this.uid = _uniqueId('chart'); 49 | this.laneSizes = 50; 50 | this.laneBuffer = 5; 51 | this.trackBuffer = 25; 52 | this.offset = undefined; 53 | this.canvas = canvas; 54 | this.ctx = ctx; 55 | 56 | // scale defaults 57 | this.scale = {}; 58 | this.scale.pretty = true; 59 | this.scale.max = undefined; 60 | this.scale.min = undefined; 61 | this.scale.auto = true; 62 | this.scale.userControlled = false; 63 | this.scale.positions = [0]; // by default scale goes on top 64 | this.scale.off = false; 65 | this.scale.size = 15; // in pixels 66 | this.scale.font = {}; 67 | this.scale.font.size = 15; // in pixels 68 | this.scale.font.color = 'black'; 69 | this.scale.font.buffer = 10; // in pixels - buffer between two scale numbers 70 | // (e.g. 1k and 2k) 71 | 72 | // glyph defaults 73 | this.glyph = {}; 74 | this.glyph.roundness = 6; 75 | this.glyph.borderWidth = 1; // in pixels 76 | this.glyph.color = ['#99CCFF', 'rgb(63, 128, 205)']; 77 | this.glyph.text = {}; 78 | this.glyph.text.color = 'black'; 79 | this.glyph.text.size = '13'; // in pixels 80 | this.glyph.text.font = 'arial'; 81 | this.glyph.text.align = 'center'; 82 | 83 | 84 | // initialize common types 85 | this.gene = {}; 86 | this.gene.text = {}; 87 | this.protein = {}; 88 | this.protein.text = {}; 89 | 90 | // event defaults 91 | this.events = {}; 92 | this.events.hasClick = false; 93 | this.events.hasMouseover = false; 94 | this.events.clicks = new Array; 95 | this.events.mouseovers = new Array; 96 | this.events.added = false; 97 | this.mouseHandler = function(e) { 98 | chart.handleMouseEvent(e, 'mouseover') 99 | }; 100 | this.clickHandler = function(e) { chart.handleMouseEvent(e, 'click') }; 101 | 102 | // tick defaults 103 | this.tick = {}; 104 | this.tick.auto = true; 105 | this.tick.major = {}; 106 | this.tick.major.size = 10; // width between major ticks in nucleotides 107 | this.tick.major.color = 'black'; 108 | this.tick.minor = {}; 109 | this.tick.minor.size = 1; // width between minor ticks in nucleotides 110 | this.tick.minor.color = 'rgb(55,55,55)'; 111 | this.tick.halfColor = 'rgb(10,10,10)'; 112 | 113 | // tooltip defaults 114 | this.tooltips = {}; 115 | this.tooltips.text = {} 116 | this.tooltips.text.font = 'arial'; 117 | this.tooltips.text.size = 12; // in pixels 118 | this.tooltips.borderWidth = 1; // in pixels 119 | this.tooltips.roundness = 5; // in pixels 120 | this.tooltips.fade = false; 121 | this.tooltips.style = 'light'; // also a 'dark' option 122 | this.lastToolTips = []; 123 | 124 | // scroll defaults 125 | this.scrollable = false; 126 | this.scrollValues = [0, undefined]; // values in nts where scroll 127 | 128 | this.chars = {}; 129 | this.chars.drawOnBuild = []; 130 | 131 | // draw defaults 132 | this.drawStyle = 'expand'; 133 | 134 | // draw hooks 135 | this.glyphHooks = []; 136 | this.trackHooks = []; 137 | 138 | // private variables 139 | this.myMouseEventHandler = new MouseEventHandler(this); 140 | this.tracks = []; 141 | var scaleSize = this.scale.size; 142 | var scaleFontSize = this.scale.font.size 143 | }, 144 | 145 | /** **getScaleHeight** 146 | 147 | * _Get the height of the scale/ruler_ 148 | 149 | * @return {Int} height in pixels 150 | * @api public 151 | */ 152 | getScaleHeight: function() { 153 | return (this.scale.font.size + this.scale.size); 154 | }, 155 | 156 | /** **getHeight** 157 | 158 | * _Get the height of the entire Scribl chart/view_ 159 | 160 | * @return {Int} height in pixels 161 | * @api public 162 | */ 163 | getHeight: function() { 164 | var wholeHeight = 0; 165 | 166 | if (!this.scale.off) wholeHeight += this.getScaleHeight(); 167 | var numTracks = this.tracks.length 168 | 169 | for (var i=0; i < numTracks; i++) { 170 | wholeHeight += this.trackBuffer; 171 | wholeHeight += this.tracks[i].getHeight(); 172 | } 173 | 174 | return wholeHeight; 175 | }, 176 | 177 | /** **getFeatures** 178 | 179 | * _Returns an array of features (e.g. gene)_ 180 | 181 | * @return {Array} of features 182 | * @api public 183 | */ 184 | 185 | getFeatures: function() { 186 | var features = []; 187 | for (var i=0; i < this.tracks.length; i++){ 188 | for (var k=0; k < this.tracks[i].lanes.length; k++) { 189 | features = features.concat(this.tracks[i].lanes[k].features); 190 | } 191 | } 192 | return features; 193 | }, 194 | 195 | /** **setCanvas** 196 | 197 | * _Changes the canvas that Scribl draws to_ 198 | 199 | * @param {Html Canvas Element} the canvas to draw to 200 | * @api public 201 | */ 202 | setCanvas: function(canvas){ 203 | this.canvas = canvas; 204 | this.ctx = canvas.getContext('2d'); 205 | // this.registerEventListeners(); 206 | }, 207 | 208 | /** **addScale** 209 | 210 | * _Inserts a scale at the end of the last track currently added to the chart_ 211 | 212 | * @api public 213 | */ 214 | addScale: function() { 215 | if (this.scale.userControlled) 216 | this.scale.positions.push( this.tracks.length ); 217 | else { 218 | this.scale.positions = [ this.tracks.length ]; 219 | this.scale.userControlled = true; 220 | } 221 | }, 222 | 223 | /** **addTrack** 224 | 225 | * _Creates a new track and adds it to the Scribl chart/view_ 226 | 227 | * @return {Object} the new track 228 | * @api public 229 | */ 230 | addTrack: function() { 231 | var track = new Track(this); 232 | if (this.tracks.length == 1 && this.tracks[0] == undefined) 233 | this.tracks = []; 234 | this.tracks.push(track); 235 | return track; 236 | }, 237 | 238 | /** **removeTrack** 239 | 240 | * _removes a track_ 241 | 242 | * @param {Object} the track to be removed 243 | * @api public 244 | */ 245 | removeTrack: function(track) { 246 | var chart = this; 247 | 248 | for (var i=0; i < chart.tracks.length; i++){ 249 | if (track.uid == chart.tracks[i].uid) 250 | chart.tracks.splice(i,1); 251 | } 252 | delete track; 253 | }, 254 | 255 | 256 | /** **loadGenbank** 257 | 258 | * _parses a genbank file and adds the features to the Scribl chart/view_ 259 | 260 | * @param {String} genbank file as a string 261 | * @api public 262 | */ 263 | loadGenbank: function(file) { 264 | genbank(file, this); 265 | }, 266 | 267 | /** **loadBed** 268 | 269 | * _parses a bed file and adds the features to the Scribl chart/view_ 270 | 271 | * @param {String} bed file as a string 272 | * @api public 273 | */ 274 | loadBed: function(file) { 275 | bed(file, this); 276 | }, 277 | 278 | /** **loadBam** 279 | 280 | * _parses a bam file and adds the features to the Scribl chart/view_ 281 | 282 | * @param {File} bam file as a javascript file object 283 | * @param {File} bai (bam index) file as a javascript file object 284 | * @param {Int} start 285 | * @param {Int} end 286 | * @api public 287 | */ 288 | loadBam: function(bamFile, baiFile, chr, start, end, callback) { 289 | var scribl = this; 290 | // scribl.scale.min = start; 291 | // scribl.scale.max = end; 292 | var track = scribl.addTrack(); 293 | track.status = 'waiting'; 294 | makeBam(new BlobFetchable(bamFile), 295 | new BlobFetchable(baiFile), 296 | function(bam, reader) { 297 | scribl.file = bam; 298 | bam.fetch(chr, start, end, function(r, e) { 299 | if (r) { 300 | for (var i = 0; i < r.length; i += 1) { 301 | track.addFeature( new BlockArrow('bam', r[i].pos, r[i].lengthOnRef, '+', {'seq':r[i].seq})) 302 | } 303 | track.status = "received"; 304 | if (track.drawOnResponse) 305 | scribl.redraw(); 306 | //callback(); 307 | } 308 | if (e) { 309 | alert('error: ' + e); 310 | } 311 | }); 312 | }); 313 | return track; 314 | }, 315 | 316 | /** **loadFeatures** 317 | 318 | * _adds the features to the Scribl chart/view_ 319 | 320 | * @param {Array} features - array of features, which can be any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 321 | * @api public 322 | */ 323 | loadFeatures: function(features) { 324 | for ( var i=0; i < features.length; i++ ) 325 | this.addFeature( features[i] ); 326 | }, 327 | 328 | /** **addGene** 329 | 330 | * _syntactic sugar function to add a feature with the gene type_ 331 | 332 | * @param {Int} position - start position of the feature 333 | * @param {Int} length - length of the feature 334 | * @param {String} strand - '+' or '-' strand 335 | * @param {Hash} [opts] - optional hash of options that can be applied to feature 336 | * @return {Object} feature - a feature with the 'feature' type 337 | * @api public 338 | */ 339 | addGene: function (position, length, strand, opts) { 340 | return (this.addFeature( 341 | new BlockArrow('gene', position, length, strand, opts) 342 | )); 343 | }, 344 | 345 | /** **addProtein** 346 | 347 | * _syntactic sugar function to add a feature with the protein type_ 348 | 349 | * @param {Int} position - start position of the protein 350 | * @param {Int} length - length of the protein 351 | * @param {String} strand - '+' or '-' strand 352 | * @param {Hash} [opts] - optional hash of options that can be applied to protein 353 | * @return {Object} protein - a feature with the 'protein' type 354 | * @api public 355 | */ 356 | addProtein: function(position, length, strand, opts) { 357 | return (this.addFeature( 358 | new BlockArrow('protein', position, length, strand, opts) 359 | )); 360 | }, 361 | 362 | /** **addFeature** 363 | 364 | * _addFeature to Scribl chart/view and let Scribl manage track and lane placement to avoid overlaps_ 365 | 366 | * example: 367 | * `chart.addFeature( new Rect('complex',3500, 2000) );` 368 | 369 | * @param {Object} feature - any of the derived Glyph classes (e.g. Rect, Arrow, etc..) 370 | * @return {Object} feature 371 | * @api public 372 | */ 373 | addFeature: function( feature ) { 374 | var track = this.tracks[0] || this.addTrack(); 375 | track.addFeature(feature); 376 | return feature; 377 | }, 378 | 379 | 380 | /** **slice** 381 | 382 | * _slices the Scribl chart/view at given places and returns a smaller chart/view_ 383 | 384 | * @param {Int} from - nucleotide position to slice from 385 | * @param {Int} to - nucleotide position to slice to 386 | * @param {String} type - _inclusive_ (defaulte) includes any feature that has any part in region, _exclusive_, includes only features that are entirely in the region, _strict_ if feature is partly in region, it'll cut that feature at the boundary and include the cut portion 387 | * @return {Object} Scribl 388 | * @api public 389 | */ 390 | slice: function(from, to, type) { 391 | type = type || 'inclusive'; 392 | var chart = this; 393 | var sliced_features = []; 394 | 395 | // iterate through tracks 396 | var numTracks = this.tracks.length; 397 | var newChart = new Scribl(this.canvas, this.width); 398 | 399 | // TODO: make this more robust 400 | newChart.scale.min = this.scale.min; 401 | newChart.scale.max = this.scale.max; 402 | newChart.offset = this.offset; 403 | newChart.scale.off = this.scale.off; 404 | newChart.scale.pretty = this.scale.pretty; 405 | newChart.laneSizes = this.laneSizes; 406 | newChart.drawStyle = this.drawStyle; 407 | newChart.glyph = this.glyph; 408 | newChart.glyphHooks = this.glyphHooks; 409 | newChart.trackHooks = this.trackHooks; 410 | // newChart.mouseHandler = this.mouseHandler; 411 | // newChart.clickHandler = this.clickHandler; 412 | newChart.previousDrawStyle = this.previousDrawStyle; 413 | 414 | // for ( var i in object.getOwnPropertyNames(this) ) { 415 | // newChart[i] = this[i]; 416 | // } 417 | 418 | // Aliases for the rather verbose methods on ES5 419 | // var descriptor = Object.getOwnPropertyDescriptor 420 | // , properties = Object.getOwnPropertyNames 421 | // , define_prop = Object.defineProperty 422 | 423 | // (target:Object, source:Object) → Object 424 | // Copies properties from `source' to `target' 425 | 426 | // properties(chart).forEach(function(key) { 427 | // define_prop(newChart, key, descriptor(chart, key)) }) 428 | 429 | 430 | for ( var j=0; j < numTracks; j++) { 431 | var track = this.tracks[j]; 432 | var newTrack = newChart.addTrack(); 433 | newTrack.drawStyle = track.drawStyle; 434 | var numLanes = track.lanes.length; 435 | for ( var i=0; i < numLanes; i++ ) { 436 | var newLane = newTrack.addLane(); 437 | var s_features = track.lanes[i].features; 438 | for (var k=0; k < s_features.length; k++ ) { 439 | var end = s_features[k].position + s_features[k].length; 440 | var start = s_features[k].position; 441 | // determine if feature is in slice/region 442 | if(type == 'inclusive') { 443 | if ( start >= from && start <= to ) 444 | newLane.addFeature( s_features[k].clone() ) 445 | else if ( end > from && end < to ) 446 | newLane.addFeature( s_features[k].clone() ) 447 | else if ( start < from && end > to ) 448 | newLane.addFeature( s_features[k].clone() ) 449 | else if ( start > from && end < to) 450 | newLane.addFeature( s_features[k].clone() ) 451 | } else if (type == 'strict') { 452 | if ( start >= from && start <= to){ 453 | if (end > from && end < to) 454 | newLane.addFeature( s_features[k].clone() ) 455 | else { 456 | // turn first half into rect to stop having two block arrows features 457 | if (s_features[k].glyphType == "BlockArrow" && s_features[k].strand == "+") 458 | var f = s_features[k].clone("Rect"); 459 | else 460 | var f = s_features[k].clone(); 461 | 462 | f.length = Math.abs(to - start); 463 | newLane.addFeature( f ); 464 | } 465 | } else if (end > from && end < to) { 466 | // turn first half into rect to stop having two block arrows features 467 | if (s_features[k].glyphType == "BlockArrow" && s_features[k].strand == "-") 468 | var f = s_features[k].clone("Rect"); 469 | else 470 | var f = s_features[k].clone(); 471 | 472 | f.position = from; 473 | f.length = Math.abs(end - from); 474 | newLane.addFeature( f ); 475 | } 476 | else if( start < from && end > to){ 477 | // turn first half into rect to stop having two block arrows features 478 | if (s_features[k].glyphType == "BlockArrow") 479 | var f = s_features[k].clone("Rect"); 480 | else 481 | var f = s_features[k].clone(); 482 | f.position = from; 483 | f.length = Math.abs(to - from); 484 | newLane.addFeature( f ); 485 | } 486 | } else if (type == 'exclusive') { 487 | if ( start >= from && start <= to && end > from && end < to) 488 | newLane.addFeature( s_features[k].clone() ) 489 | } 490 | 491 | } 492 | 493 | } 494 | } 495 | 496 | 497 | // for (var attr in this) { 498 | // if (this.hasOwnProperty(attr)) copy[attr] = this[attr]; 499 | // } 500 | 501 | return newChart; 502 | }, 503 | 504 | /** **draw** 505 | 506 | * _draws everything_ 507 | 508 | * @api public 509 | */ 510 | 511 | draw: function() { 512 | // initalize variables 513 | var ctx = this.ctx; 514 | var tracks = this.tracks; 515 | 516 | // check if scrollable 517 | if (this.scrollable == true) { 518 | this.initScrollable(); 519 | } 520 | 521 | ctx.save(); 522 | // make scale pretty by starting and ending the scale 523 | // at major ticks and choosing best tick distances 524 | this.initScale(); 525 | 526 | // fix offsets so scale will not be cut off on left side 527 | // check if offset is turned off and then set it to static '0' 528 | if (this.offset == undefined) 529 | this.offset = Math.ceil( ctx.measureText('0').width/2 + 10 ); 530 | 531 | // ctx.save(); 532 | 533 | ctx.save(); 534 | 535 | // draw tracks 536 | for (var i=0; i 0) 565 | this.draw(); 566 | }, 567 | 568 | /** **initScale** 569 | 570 | * _initializes scale_ 571 | 572 | * @api internal 573 | */ 574 | initScale: function() { 575 | if (this.scale.pretty) { 576 | 577 | // determine reasonable tick intervals 578 | if (this.tick.auto) { 579 | // set major tick interval 580 | this.tick.major.size = this.determineMajorTick(); 581 | 582 | // set minor tick interval 583 | this.tick.minor.size = Math.round(this.tick.major.size / 10); 584 | } 585 | 586 | // make scale end on major ticks 587 | if (this.scale.auto) { 588 | this.scale.min -= this.scale.min % this.tick.major.size; 589 | this.scale.max = Math.round(this.scale.max / this.tick.major.size + .4) 590 | * this.tick.major.size; 591 | } 592 | } 593 | }, 594 | 595 | /** **drawScale** 596 | 597 | * _draws scale_ 598 | 599 | * @api public 600 | */ 601 | drawScale: function(options){ 602 | var firstMinorTick; 603 | var ctx = this.ctx; 604 | var fillStyleRevert = ctx.fillStyle; 605 | 606 | if(options && options.init) 607 | this.initScale(); 608 | 609 | // determine tick vertical sizes and vertical tick positions 610 | var tickStartPos = this.scale.font.size + this.scale.size; 611 | var majorTickEndPos = this.scale.font.size + 2; 612 | var minorTickEndPos = this.scale.font.size + this.scale.size * 0.66; 613 | var halfTickEndPos = this.scale.font.size + this.scale.size * 0.33; 614 | 615 | // set scale defaults 616 | ctx.font = this.scale.font.size + 'px arial'; 617 | ctx.textBaseline = 'top'; 618 | ctx.fillStyle = this.scale.font.color; 619 | 620 | if (this.offset == undefined) 621 | this.offset = Math.ceil( ctx.measureText('0').width/2 + 10 ); 622 | 623 | // determine the place to start first minor tick 624 | if (this.scale.min % this.tick.minor.size == 0) 625 | firstMinorTick = this.scale.min 626 | else 627 | firstMinorTick = this.scale.min - (this.scale.min % this.tick.minor.size) 628 | + this.tick.minor.size; 629 | 630 | // draw 631 | for(var i = firstMinorTick; i <= this.scale.max; i += this.tick.minor.size){ 632 | ctx.beginPath(); 633 | if(i == 187250) 634 | var h = 2; 635 | var curr_pos = this.pixelsToNts(i - this.scale.min) + this.offset; 636 | if ( i % this.tick.major.size == 0) { // draw major tick 637 | // create text 638 | var tickText = this.getTickText(i); 639 | ctx.textAlign = 'center'; 640 | ctx.fillText( tickText , curr_pos, 0 ); 641 | 642 | // create major tick 643 | ctx.moveTo( curr_pos, tickStartPos ); 644 | ctx.lineTo( curr_pos, majorTickEndPos ); 645 | ctx.strokeStyle = this.tick.major.color; 646 | ctx.stroke(); 647 | 648 | } else { // draw minor tick 649 | ctx.moveTo( curr_pos, tickStartPos ); 650 | 651 | // create half tick - tick between two major ticks 652 | if ( i % (this.tick.major.size/2) == 0 ) { 653 | ctx.strokeStyle = this.tick.halfColor; 654 | ctx.lineTo( curr_pos, halfTickEndPos ); 655 | } 656 | // create minor tick 657 | else{ 658 | ctx.strokeStyle = this.tick.minor.color; 659 | ctx.lineTo( curr_pos, minorTickEndPos ); 660 | } 661 | ctx.stroke(); 662 | } 663 | } 664 | 665 | // restore fillstyle 666 | ctx.fillStyle = fillStyleRevert; 667 | 668 | // shift down size of scale 669 | ctx.translate(0, this.getScaleHeight() + this.laneBuffer); 670 | }, 671 | 672 | /** **pixelsToNts** 673 | 674 | * _Get the number of nucleotides per the given pixels_ 675 | 676 | * @param {Int} [pixels] optional - if not given, the ratio of pixels/nts will be returned 677 | * @return {Int} nucleotides or pixels/nts ratio 678 | * @api internal 679 | */ 680 | pixelsToNts: function(pixels) { 681 | if (pixels == undefined) 682 | return ( this.width / ( this.scale.max - this.scale.min) ); 683 | else 684 | return ( this.width / ( this.scale.max - this.scale.min) * pixels ); 685 | }, 686 | 687 | /** **ntsToPixels** 688 | 689 | * _Get the number of pixels shown per given nucleotides_ 690 | 691 | * @param {Int} [nucleotides] optional - if not given, the ratio of nts/pixel will be returned 692 | * @return {Int} pixels or nts/pixel ratio 693 | * @api internal 694 | */ 695 | ntsToPixels: function(nts) { 696 | if (nts == undefined) 697 | return ( 1 / this.pixelsToNts() ); 698 | else 699 | return ( nts / this.width ); 700 | }, 701 | 702 | /** **initScrollable** 703 | 704 | * _turns static chart into scrollable chart_ 705 | 706 | * @api internal 707 | */ 708 | initScrollable: function() { 709 | var scrollStartMin; 710 | 711 | if (!this.scrolled){ 712 | // create divs 713 | var parentDiv = document.createElement('div'); 714 | var canvasContainer = document.createElement('div'); 715 | var sliderDiv = document.createElement('div'); 716 | sliderDiv.id = 'scribl-zoom-slider'; 717 | sliderDiv.className = 'slider'; 718 | sliderDiv.style.cssFloat = 'left'; 719 | sliderDiv.style.height = (new String(this.canvas.height * .5)) + 'px'; 720 | sliderDiv.style.margin = '30px auto auto -20px' 721 | 722 | // grab css styling from canavs 723 | parentDiv.style.cssText = this.canvas.style.cssText; 724 | this.canvas.style.cssText = ''; 725 | parentWidth = parseInt(this.canvas.width) + 25; 726 | parentDiv.style.width = parentWidth + 'px'; 727 | canvasContainer.style.width = this.canvas.width + 'px'; 728 | canvasContainer.style.overflow = 'auto'; 729 | canvasContainer.id = 'scroll-wrapper'; 730 | 731 | 732 | 733 | this.canvas.parentNode.replaceChild(parentDiv, this.canvas); 734 | parentDiv.appendChild(sliderDiv); 735 | canvasContainer.appendChild(this.canvas); 736 | parentDiv.appendChild(canvasContainer); 737 | 738 | jQuery(canvasContainer).dragscrollable({dragSelector: 'canvas:first', acceptPropagatedEvent: false}); 739 | } 740 | 741 | var totalNts = this.scale.max - this.scale.min; 742 | var scrollStartMax = this.scrollValues[1] || this.scale.max - totalNts * .35; 743 | if( this.scrollValues[0] != undefined) 744 | scrollStartMin = this.scrollValues[0]; 745 | else 746 | scrollStartMin = this.scale.max + totalNts * .35; 747 | 748 | var viewNts = scrollStartMax - scrollStartMin; 749 | var viewNtsPerPixel = viewNts / document.getElementById('scroll-wrapper').style.width.split('px')[0]; 750 | 751 | var canvasWidth = (totalNts / viewNtsPerPixel) || 100; 752 | this.canvas.width = canvasWidth; 753 | this.width = canvasWidth - 30; 754 | schart = this; 755 | var zoomValue = (scrollStartMax - scrollStartMin) / (this.scale.max - this.scale.min) * 100 || 1; 756 | 757 | jQuery(sliderDiv).slider({ 758 | orientation: 'vertical', 759 | range: 'min', 760 | min: 6, 761 | max: 100, 762 | value: zoomValue, 763 | slide: function( event, ui ) { 764 | var totalNts = schart.scale.max - schart.scale.min; 765 | var width = ui['value'] / 100 * totalNts; 766 | var widthPixels = ui['value'] / 100 * schart.canvas.width; 767 | var canvasContainer = document.getElementById('scroll-wrapper'); 768 | var center = canvasContainer.scrollLeft + parseInt(canvasContainer.style.width.split('px')[0]) / 2; 769 | 770 | // get min max pixels 771 | var minPixel = center - widthPixels/2; 772 | var maxPixel = center + widthPixels/2; 773 | 774 | // convert to nt 775 | var min = schart.scale.min + (minPixel / schart.canvas.width) * totalNts; 776 | var max = schart.scale.min + (maxPixel / schart.canvas.width) * totalNts; 777 | 778 | schart.scrollValues = [min, max]; 779 | schart.ctx.clearRect(0, 0, schart.canvas.width, schart.canvas.height); 780 | schart.draw(); 781 | } 782 | }); 783 | 784 | 785 | var startingPixel = (scrollStartMin - this.scale.min) / totalNts * this.canvas.width; 786 | document.getElementById('scroll-wrapper').scrollLeft = startingPixel; 787 | this.scrolled = true; 788 | }, 789 | 790 | 791 | /** **determineMajorTick** 792 | 793 | * _intelligently determines a major tick interval based on size of the chart/view and size of the numbers on the scale_ 794 | 795 | * @return {Int} major tick interval 796 | * @api internal 797 | */ 798 | determineMajorTick: function() { 799 | this.ctx.font = this.scale.font.size + 'px arial'; 800 | var numtimes = this.width/(this.ctx.measureText(this.getTickTextDecimalPlaces(this.scale.max)).width + this.scale.font.buffer); 801 | 802 | // figure out the base of the tick (e.g. 2120 => 2000) 803 | var irregularTick = (this.scale.max - this.scale.min) / numtimes; 804 | var baseNum = Math.pow(10, parseInt(irregularTick).toString().length -1); 805 | this.tick.major.size = Math.ceil(irregularTick / baseNum) * baseNum; 806 | 807 | // round up to a 5* or 1* number (e.g 5000 or 10000) 808 | var digits = (this.tick.major.size + '').length; 809 | var places = Math.pow(10, digits); 810 | var first_digit = this.tick.major.size / places; 811 | 812 | if (first_digit > .1 && first_digit <= .5) 813 | first_digit = .5; 814 | else if (first_digit > .5) 815 | first_digit = 1; 816 | 817 | // return major tick interval 818 | return (first_digit * places); 819 | }, 820 | 821 | 822 | /** **getTickText** 823 | 824 | * _abbreviates tick text numbers using 'k', or 'm' (e.g. 10000 becomes 10k)_ 825 | 826 | * @param {Int} tickNumber - the tick number that needs to be abbreviated 827 | * @return {String} abbreviated tickNumber 828 | * @api internal 829 | */ 830 | getTickText: function(tickNumber) { 831 | if ( !this.tick.auto ) 832 | return tickNumber; 833 | 834 | var tickText = tickNumber; 835 | if (tickNumber >= 1000000 ) { 836 | var decPlaces = 5; 837 | var base = Math.pow(10, decPlaces) 838 | tickText = Math.round(tickText / 1000000 * base) / base + 'm'; // round to decPlaces 839 | } else if ( tickNumber >= 1000 ) { 840 | var decPlaces = 2; 841 | var base = Math.pow(10, decPlaces) 842 | tickText = Math.round(tickText / 1000 * base) / base + 'k'; 843 | } 844 | 845 | return tickText; 846 | }, 847 | 848 | /** **getTickTextDecimalPlaces** 849 | 850 | * _determines the tick text with decimal places_ 851 | 852 | * @param {Int} tickNumber - the tick number that needs to be abbreviated 853 | * @return {String} abbreviated tickNumber 854 | * @api internal 855 | */ 856 | getTickTextDecimalPlaces: function(tickNumber){ 857 | if ( !this.tick.auto ) 858 | return tickNumber; 859 | 860 | var tickText = tickNumber; 861 | if (tickNumber >= 1000000 ) { 862 | var decPlaces = 5; 863 | tickText = Math.round( tickText / (1000000 / Math.pow(10,decPlaces)) ) + 'm'; // round to 2 decimal places 864 | } else if ( tickNumber >= 1000 ){ 865 | var decPlaces = 2; 866 | tickText = Math.round( tickText / (1000 / Math.pow(10,decPlaces)) ) + 'k'; 867 | } 868 | 869 | return tickText; 870 | }, 871 | 872 | /** **handleMouseEvent** 873 | 874 | * _handles mouse events_ 875 | 876 | * @param {Object} event - triggered event 877 | * @param {String} type - type of event 878 | * @api internal 879 | */ 880 | handleMouseEvent: function(e, type) { 881 | this.myMouseEventHandler.setMousePosition(e); 882 | var positionY = this.myMouseEventHandler.mouseY; 883 | var lane; 884 | 885 | for( var i=0; i < this.tracks.length; i++) { 886 | for( var k=0; k < this.tracks[i].lanes.length; k++) { 887 | var yt = this.tracks[i].lanes[k].getPixelPositionY(); 888 | var yb = yt + this.tracks[i].lanes[k].getHeight(); 889 | if (positionY >= yt && positionY <= yb ) { 890 | lane = this.tracks[i].lanes[k]; 891 | break; 892 | } 893 | } 894 | } 895 | 896 | // if mouse is not on any tracks then return 897 | if (!lane) return; 898 | 899 | var drawStyle = lane.track.getDrawStyle(); 900 | 901 | if (drawStyle == 'collapse') { 902 | this.redraw(); 903 | } else if (drawStyle == 'line') { 904 | // do nothing 905 | } else { 906 | this.ctx.save(); 907 | lane.erase(); 908 | this.ctx.translate(0, lane.getPixelPositionY()); 909 | lane.draw(); 910 | var ltt; 911 | while (ltt = this.lastToolTips.pop() ) { 912 | this.ctx.putImageData(ltt.pixels, ltt.x, ltt.y ) 913 | } 914 | this.ctx.restore(); 915 | } 916 | 917 | 918 | var chart = this; 919 | 920 | if (type == 'click') { 921 | var clicksFns = chart.events.clicks; 922 | for (var i = 0; i < clicksFns.length; i++) 923 | clicksFns[i](chart); 924 | } else { 925 | var mouseoverFns = chart.events.mouseovers; 926 | for (var i = 0; i < mouseoverFns.length; i++) 927 | mouseoverFns[i](chart); 928 | } 929 | 930 | this.myMouseEventHandler.reset(chart); 931 | 932 | 933 | }, 934 | 935 | 936 | /** **addClickEventListener** 937 | 938 | * _add's function that will execute each time a feature is clicked_ 939 | 940 | * @param {Function} func - function to be triggered 941 | * @api public 942 | */ 943 | addClickEventListener: function(func) { 944 | this.events.clicks.push(func); 945 | }, 946 | 947 | /** **addMouseoverEventListener** 948 | 949 | * _add's function that will execute each time a feature is mouseovered_ 950 | 951 | * @param {Function} func - function to be triggered 952 | * @api public 953 | */ 954 | addMouseoverEventListener: function(func) { 955 | this.events.mouseovers.push(func); 956 | }, 957 | 958 | /** **removeEventListeners** 959 | 960 | * _remove event listerners_ 961 | 962 | * @param {String} event-type - e.g. mouseover, click, etc... 963 | * @api internal 964 | */ 965 | removeEventListeners: function(eventType){ 966 | if (eventType == 'mouseover') 967 | this.canvas.removeEventListener('mousemove', this.mouseHandler); 968 | else if (eventType == 'click') 969 | this.canvas.removeEventListener('click', this.clickHandler); 970 | }, 971 | 972 | 973 | /** **registerEventListeners** 974 | 975 | * _adds event listerners_ 976 | 977 | * @api internal 978 | */ 979 | registerEventListeners: function() { 980 | var chart = this; 981 | 982 | if ( this.events.mouseovers.length > 0) { 983 | this.canvas.removeEventListener('mousemove', chart.mouseHandler); 984 | this.canvas.addEventListener('mousemove', chart.mouseHandler, false); 985 | } 986 | if ( this.events.clicks.length > 0 ) { 987 | $(this.canvas).unbind('click'); 988 | $(this.canvas).bind('click', function(e) {chart.handleMouseEvent(e, 'click')}) 989 | // this.canvas.removeEventListener('click', chart.clickHandler); 990 | // this.canvas.addEventListener('click', chart.clickHandler, false); 991 | } 992 | this.events.added = true; 993 | } 994 | 995 | 996 | }); 997 | --------------------------------------------------------------------------------