diff --git a/.gitignore b/.gitignore index 18da204..134f0cc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ dist/alignment.js *.ttf *.eot tags* +.tern-port diff --git a/dist/data/sorted.bam b/dist/data/sorted.bam new file mode 100644 index 0000000..2be0127 Binary files /dev/null and b/dist/data/sorted.bam differ diff --git a/dist/data/sorted.bam.bai b/dist/data/sorted.bam.bai new file mode 100644 index 0000000..39ac0b8 Binary files /dev/null and b/dist/data/sorted.bam.bai differ diff --git a/package.json b/package.json index 9920779..dc063bc 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,9 @@ { "name": "alignment.js", - "version": "0.0.1", + "version": "0.1.0", "main": "lib/alignment.js", "license": "MIT", "dependencies": { - "@babel/plugin-proposal-class-properties": "^7.4.4", "@gmod/bam": "^1.0.18", "bootstrap": "^4.1.1", "d3": "5", @@ -12,11 +11,11 @@ "express": "^4.16.4", "file-saver": "^2.0.0", "jquery": "^3.3.1", - "phylotree": "1.0.0-alpha.14", + "phylotree": "1.0.0-alpha.17", "popper.js": "^1.14.3", "react": "^16.2.0", "react-dom": "^16.2.0", - "react-phylotree": "^0.0.1", + "react-phylotree": "0.1.0", "react-router-dom": "^4.3.1", "save-svg-as-png": "^1.4.14", "text-width": "^1.2.0", @@ -25,6 +24,7 @@ "devDependencies": { "@babel/cli": "^7.4.4", "@babel/core": "^7.4.5", + "@babel/plugin-proposal-class-properties": "^7.4.4", "@babel/preset-env": "^7.4.5", "@babel/preset-react": "^7.0.0", "autoprefixer": "^8.4.1", diff --git a/src/Alignment.jsx b/src/Alignment.jsx index ec46ebc..29b40b6 100644 --- a/src/Alignment.jsx +++ b/src/Alignment.jsx @@ -43,7 +43,7 @@ class Alignment extends Component { } initialize(props) { if (props.fasta) { - const { fasta, site_size, width, height, axis_height } = props; + const { fasta, site_size, width, height } = props; this.sequence_data = fastaParser(fasta); const { sequence_data } = this; const { label_padding } = this.props; @@ -84,6 +84,7 @@ class Alignment extends Component { sequence_data={this.sequence_data} x_pixel={this.x_pixel} scroll_broadcaster={this.scroll_broadcaster} + start_site={this.props.start_site} /> mol + molecule: mol => mol, + start_site: 0, + onSequenceClick: (label, i) => () => null }; export default Alignment; diff --git a/src/TreeAlignment.jsx b/src/TreeAlignment.jsx new file mode 100644 index 0000000..ecbc29e --- /dev/null +++ b/src/TreeAlignment.jsx @@ -0,0 +1,77 @@ +import React, { Component } from "react"; + +import Placeholder from "./components/Placeholder.jsx"; +import SiteAxis from "./components/SiteAxis.jsx"; +import BaseAlignment from "./components/BaseAlignment.jsx"; +import Tree from "./components/Tree.jsx"; +import ScrollBroadcaster from "./helpers/ScrollBroadcaster"; +import { nucleotide_color, nucleotide_text_color } from "./helpers/colors"; + +function TreeAlignment(props) { + const { sequence_data } = props; + if (!sequence_data) return
; + const { width, tree_width, height, axis_height, site_size } = props, + full_pixel_width = sequence_data + ? sequence_data[0].seq.length * site_size + : null, + full_pixel_height = sequence_data ? sequence_data.length * site_size : null, + alignment_width = full_pixel_width + ? Math.min(full_pixel_width, width - tree_width) + : width, + alignment_height = full_pixel_height + ? Math.min(full_pixel_height, height - axis_height) + : height, + scroll_broadcaster = new ScrollBroadcaster({ + width: full_pixel_width, + height: full_pixel_height, + x_pad: width - tree_width, + y_pad: height - axis_height, + bidirectional: [ + "alignmentjs-alignment", + "alignmentjs-axis-div", + "alignmentjs-tree-div" + ] + }); + return ( +
+ + + + +
+ ); +} + +TreeAlignment.defaultProps = { + site_color: nucleotide_color, + text_color: nucleotide_text_color, + label_padding: 10, + site_size: 20, + axis_height: 25, + width: 960, + tree_width: 500, + height: 500, + sender: "main", + molecule: mol => mol +}; + +export default TreeAlignment; diff --git a/src/app.jsx b/src/app.jsx index c078e8c..2220611 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -10,6 +10,7 @@ import * as FASTA from "./app/FASTA.jsx"; import * as FNA from "./app/FNA.jsx"; import * as BAM from "./app/BAM.jsx"; import Components from "./app/Components.jsx"; +import PreventDefaultPatch from "./prevent_default_patch"; import "./app/styles.scss"; function Divider(props) { @@ -86,6 +87,7 @@ function BAMLinks(props) { + ); } @@ -136,6 +138,7 @@ class App extends Component { +
@@ -151,57 +154,7 @@ function Main(props) { ); } -/* Temporary fix for a breaking change in Chrome to React - * See https://github.com/facebook/react/issues/14856 - */ -const EVENTS_TO_MODIFY = [ - "touchstart", - "touchmove", - "touchend", - "touchcancel", - "wheel" -]; - -const originalAddEventListener = document.addEventListener.bind(); -document.addEventListener = (type, listener, options, wantsUntrusted) => { - let modOptions = options; - if (EVENTS_TO_MODIFY.includes(type)) { - if (typeof options === "boolean") { - modOptions = { - capture: options, - passive: false - }; - } else if (typeof options === "object") { - modOptions = { - ...options, - passive: false - }; - } - } - - return originalAddEventListener(type, listener, modOptions, wantsUntrusted); -}; - -const originalRemoveEventListener = document.removeEventListener.bind(); -document.removeEventListener = (type, listener, options) => { - let modOptions = options; - if (EVENTS_TO_MODIFY.includes(type)) { - if (typeof options === "boolean") { - modOptions = { - capture: options, - passive: false - }; - } else if (typeof options === "object") { - modOptions = { - ...options, - passive: false - }; - } - } - return originalRemoveEventListener(type, listener, modOptions); -}; -// End of temporary fix - +PreventDefaultPatch(document); ReactDOM.render(
, document.body.appendChild(document.createElement("div")) diff --git a/src/app/BAM.jsx b/src/app/BAM.jsx index 872ac6e..f3b5d6c 100644 --- a/src/app/BAM.jsx +++ b/src/app/BAM.jsx @@ -2,12 +2,32 @@ import React, { Component } from "react"; import { BamFile } from "@gmod/bam"; import Alignment from "../Alignment.jsx"; +import ScaffoldViewer from "../components/ScaffoldViewer.jsx"; +import { DataFetcher } from "./Components.jsx"; import BAMReader from "../helpers/bam.js"; +import { nucleotide_color, nucleotide_difference } from "../helpers/colors"; function VariantCaller(props) { return

Variant calling example will go here.

; } +function ScaffoldExample() { + return ( +
+

Scaffold viewer

+
    +
  • The reference sequence stays fixed to the top.
  • +
  • + Click the scaffold to navigate to that portion of the alignment. +
  • +
+ + + +
+ ); +} + class BAMViewer extends Component { constructor(props) { super(props); @@ -15,26 +35,85 @@ class BAMViewer extends Component { window_start: 0, window_width: 200, site_size: 15, - fasta: null + fasta: [], + label: null, + molecule: mol => mol, + site_color: nucleotide_color }; } componentDidMount() { const bam_file = new BamFile({ - bamUrl: "data/sorted.bam", - baiUrl: "data/sorted.bam.bai" + bamUrl: this.props.data_url, + baiUrl: this.props.data_url + ".bai" }); this.bam = new BAMReader(bam_file); - const { window_start, window_width } = this.state, - window_end = window_start + window_width; + const { window_start, window_width } = this.state; + this.loadBamWindow(window_start, window_width); + } + loadBamWindow(window_start, window_width) { + const window_end = window_start + window_width; this.bam .fasta_window(window_start, window_end) - .then(fasta => this.setState({ fasta })); + .then(fasta => this.setState({ fasta, window_start, window_width })); + } + handleStartChange(e) { + const window_start = +e.target.value, + { window_width } = this.state; + this.loadBamWindow(window_start, window_width); + } + handleWidthChange(e) { + const { window_start } = this.state, + window_width = +e.target.value; + this.loadBamWindow(window_start, window_width); } + scrollExcavator = () => { + return this.scrollExcavator.broadcaster.location(); + }; + onSequenceClick = (label, i) => { + return () => { + const { x_fraction, y_fraction } = this.scrollExcavator(), + scroll_corrector = () => { + this.scrollExcavator.broadcaster.broadcast( + x_fraction, + y_fraction, + "main" + ); + }; + if (label == this.state.label) { + this.setState( + { + label: null, + molecule: mol => mol, + site_color: nucleotide_color + }, + scroll_corrector + ); + } else { + const desired_record = this.state.fasta.filter( + datum => datum.header == label + )[0], + molecule = (mol, site, header) => { + if (mol == "-") return "-"; + if (header == desired_record.header) return mol; + return mol == desired_record.seq[site - 1] ? "." : mol; + }; + + this.setState( + { + label: label, + molecule: molecule, + site_color: nucleotide_difference(desired_record) + }, + scroll_corrector + ); + } + }; + }; render() { - const width = 1400, + const width = 1140, toolbar_style = { display: "flex", - justifyContent: "space-between", + justifyContent: "space-around", width: width }; return ( @@ -48,8 +127,8 @@ class BAMViewer extends Component { value={this.state.window_start} min={0} max={100} - step={5} - onChange={e => this.setState({ window_start: e.target.value })} + step={25} + onChange={e => this.handleStartChange(e)} /> @@ -59,8 +138,8 @@ class BAMViewer extends Component { value={this.state.window_width} min={100} max={1000} - step={5} - onChange={e => this.setState({ window_width: e.target.value })} + step={10} + onChange={e => this.handleWidthChange(e)} /> @@ -74,16 +153,27 @@ class BAMViewer extends Component { onChange={e => this.setState({ site_size: e.target.value })} /> + Number of reads in window: {this.state.fasta.length} ); } } -export { BAMViewer, VariantCaller }; +BAMViewer.defaultProps = { + data_url: "data/sorted.bam" +}; + +export default BAMViewer; +export { BAMViewer, VariantCaller, ScaffoldExample }; diff --git a/src/app/Components.jsx b/src/app/Components.jsx index 2221465..b45b3a7 100644 --- a/src/app/Components.jsx +++ b/src/app/Components.jsx @@ -51,4 +51,4 @@ function BaseSVGTreeInstance(props) { ); } -export { BaseSVGTreeInstance }; +export { DataFetcher, BaseSVGTreeInstance }; diff --git a/src/app/FNA.jsx b/src/app/FNA.jsx index c200a39..4ebf158 100644 --- a/src/app/FNA.jsx +++ b/src/app/FNA.jsx @@ -2,6 +2,7 @@ import React, { Component } from "react"; import { text } from "d3-fetch"; import { saveAs } from "file-saver"; +import TreeAlignment from "../TreeAlignment.jsx"; import { fnaParser, fnaToText } from "../helpers/fasta"; import { BaseSVGTreeInstance } from "./Components.jsx"; import Button from "../components/Button.jsx"; @@ -29,9 +30,9 @@ class FNAViewer extends Component { componentDidMount() { text("data/CD2.fna").then(data => this.loadFNA(data)); } - loadFNA(fasta) { + loadFNA(text) { this.setState({ - data: fnaParser(fasta, true), + data: fnaParser(text, true), show_differences: "" }); } @@ -43,14 +44,14 @@ class FNAViewer extends Component { } siteColor() { if (!this.state.show_differences) return nucleotide_color; - const desired_record = this.state.data.fasta.filter( + const desired_record = this.state.data.sequence_data.filter( datum => datum.header == this.state.show_differences )[0]; return nucleotide_difference(desired_record); } molecule() { if (!this.state.show_differences) return molecule => molecule; - const desired_record = this.state.data.fasta.filter( + const desired_record = this.state.data.sequence_data.filter( datum => datum.header == this.state.show_differences )[0]; return (mol, site, header) => { @@ -78,7 +79,7 @@ class FNAViewer extends Component { width: 960 }; const options = this.state.data - ? this.state.data.fasta.map(datum => { + ? this.state.data.sequence_data.map(datum => { return (